[英]How can a compiler generated default constructor be more efficient than a self-written one that does nothing but initialize members?
C.45:不要定義只初始化數據成員的默認構造函數; 改用類內成員初始值設定項
給出的理由是
原因
使用類內成員初始值設定項讓編譯器為您生成函數。 編譯器生成的函數可以更高效。
請注意,這特別是關於一個默認構造函數,它除了初始化成員之外什么都不做,並且指南建議不應編寫這樣的構造函數。
“壞”的例子是:
Example, bad class X1 { // BAD: doesn't use member initializers string s; int i; public: X1() :s{"default"}, i{1} { } // ... };
“好”的例子是使用類內成員初始值設定項並且沒有用戶聲明的構造函數:
Example class X2 { string s = "default"; int i = 1; public: // use compiler-generated default constructor // ... };
在該特定示例(或任何其他示例)中,編譯器生成的構造函數可以比用戶提供的構造函數更有效地做什么?
初始值設定項列表是否沒有提供與類內初始值設定項相同的優化機會?
如果作者包含正確的constexpr
和noexcept
狀態,則default
ed 構造函數應具有與等效初始化構造函數相同的生成程序集。
我懷疑“可以更有效”是指這樣一個事實,即,一般來說,它會生成比同等的開發人員編寫的代碼更優化的代碼,這些代碼會錯過諸如inline
、 constexpr
和noexcept
。
default
ed 構造函數執行的一個重要功能是它們解釋並推導出constexpr
和noexcept
的正確狀態
這是許多 C++ 開發人員未指定或可能未正確指定的內容。 由於核心指南針對新老 C++ 開發人員,這可能就是提到“優化”的原因。
constexpr
和noexcept
狀態可能會以不同的方式影響代碼生成:
constexpr
構造函數確保從常量表達式產生的值調用構造函數也將產生常量表達式。 這可以允許諸如不是常量的static
值之類的東西實際上不需要構造函數調用(例如,不需要靜態初始化開銷或鎖定)。 注意:這適用於本身不能存在於constexpr
上下文中的類型——只要構造函數的constexpr
是constexpr
構的。
noexcept
可以生成更好的消費代碼匯編,因為編譯器可能假設不會發生異常(因此不需要堆棧展開代碼)。 此外,諸如檢查std::is_nothrow_constructible...
模板之類的實用程序可能會生成更優化的代碼路徑。
除此之外,在類主體中定義的default
構造函數也使它們的定義對調用者可見——這允許更好的內聯(同樣,否則可能會錯過優化的機會)。
核心指南中的示例並沒有很好地展示這些優化。 但是,請考慮以下示例,它說明了一個可以從default
受益的實際示例:
class Foo {
int a;
std::unique_ptr<int> b;
public:
Foo() : a{42}, b{nullptr}{}
};
在此示例中,以下情況為真:
Foo{}
構造不是常量表達式Foo{}
構造不是noexcept
對比一下:
class Foo {
int a = 42;
std::unique_ptr<int> b = nullptr;
public:
Foo() = default;
};
從表面上看,這似乎是一樣的。 但突然之間,現在發生了以下變化:
Foo{}
是constexpr
,因為std::unique_ptr
的std::nullptr_t
構造函數是constexpr
(即使std::unique_ptr
不能用於完整的常量表達式)Foo{}
是一個noexcept
表達式您可以將生成的程序集與此Live Example進行比較。 請注意, default
情況下不需要任何指令來初始化foo
; 相反,它只是通過編譯器指令將值分配為常量(即使值不是常量)。
當然,也可以這樣寫:
class Foo {
int a;
std::unique_ptr<int> b;
public:
constexpr Foo() noexcept :a{42}, b{nullptr};
};
然而,這需要先驗知識,即Foo
能夠同時是constexpr
和noexcept
。 弄錯了會導致問題。 更糟糕的是,隨着代碼隨着時間的推移而演變, constexpr
/ noexcept
狀態可能會變noexcept
正確——這是構造函數default
會捕捉到的。
使用default
還有一個額外的好處,即隨着代碼的發展,它可能會在可能的地方添加constexpr
/ noexcept
—— 例如當標准庫添加更多constexpr
支持時。 最后一點對於作者來說每次代碼更改時都需要手動處理。
如果您取消使用類內成員初始值設定項,那么最后值得一提的一點是:除非是編譯器生成的(例如通過default
ed 構造函數),否則代碼中無法實現平凡。
class Bar {
int a;
public:
Bar() = default; // Bar{} is trivial!
};
Triviality 為潛在的優化提供了一個完全不同的方向,因為一個簡單的默認構造函數不需要對編譯器進行任何操作。 這允許編譯器在發現對象稍后被覆蓋時完全省略任何Bar{}
。
我認為重要的是假設C.45指的是常量(示例和實施):
例子,壞
class X1 { // BAD: doesn't use member initializers string s; int i; public: X1() :s{"default"}, i{1} { } // ... };
例子
class X2 { string s = "default"; int i = 1; public: // use compiler-generated default constructor // ... };
執法
(簡單)默認構造函數應該做的不僅僅是用常量初始化成員變量。
考慮到這一點,更容易證明(通過C.48 )為什么我們應該更喜歡類內初始化器而不是常量構造函數中的成員初始化器:
C.48: 對於常量初始值設定項,在構造函數中優先使用類內初始值設定項而不是成員初始值設定項
原因
明確要求在所有構造函數中使用相同的值。 避免重復。 避免維護問題。 它導致最短和最有效的代碼。
例子,壞
class X { // BAD int i; string s; int j; public: X() :i{666}, s{"qqq"} { } // j is uninitialized X(int ii) :i{ii} {} // s is "" and j is uninitialized // ... };
維護者如何知道 j 是否被故意未初始化(無論如何可能是一個壞主意)以及是否有意在一種情況下為 s 提供默認值 "" 而在另一種情況下提供 qqq (幾乎可以肯定是一個錯誤)? j(忘記初始化成員)的問題通常發生在向現有類添加新成員時。
例子
class X2 { int i {666}; string s {"qqq"}; int j {0}; public: X2() = default; // all members are initialized to their defaults X2(int ii) :i{ii} {} // s and j initialized to their defaults // ... };
替代方案:我們可以從構造函數的默認參數中獲得部分好處,這在舊代碼中並不少見。 但是,這不太明確,會導致傳遞更多參數,並且當有多個構造函數時會重復:
class X3 { // BAD: inexplicit, argument passing overhead int i; string s; int j; public: X3(int ii = 666, const string& ss = "qqq", int jj = 0) :i{ii}, s{ss}, j{jj} { } // all members are initialized to their defaults // ... };
執法
(Simple) Every constructor should initialize every member variable (either explicitly, via a delegating ctor call or via default
建造)。 (簡單)構造函數的默認參數表明類內初始化程序可能更合適。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.