编译器生成的默认构造函数如何比只初始化成员什么都不做的自写结构更有效?

for*_*818 6 c++ initialization default-constructor cpp-core-guidelines

通过这个答案,我正在阅读核心准则

C.45:不要定义仅初始化数据成员的默认构造函数;改用类内成员初始化器

给出的理由是

原因

使用类内成员初始化器可使编译器为您生成函数。编译器生成的函数可以更有效。

请注意,这是专门针对默认构造函数的,该默认构造函数除了初始化成员外不执行任何操作,并且该准则建议不要编写这样的构造函数。

“坏”的例子是:

Example, bad

class X1 { // BAD: doesn't use member initializers
    string s;
    int i;
public:
    X1() :s{"default"}, i{1} { }
    // ...
};
Run Code Online (Sandbox Code Playgroud)

在该特定示例(或任何其他示例)中,编译器生成的构造函数比用户提供的构造函数更有效率吗?

我天真地希望初始化程序列表提供与类初始化程序相同的优化机会。

Hum*_*ler 9

简答

default编构造应尽可能具有相当于初始值的构造产生组件相同的设置,提交包括正确constexprnoexcept状态

我怀疑“可以更有效地”指的是,在一般情况下,它会产生比同等更优化的代码其实开发人员编写的一个失误的机会,如inlineconstexprnoexcept

长答案

是一个重要的特征defaultED构造函数执行的是他们解释和推断两个正确的状态constexprnoexcept

这是许多 C++ 开发人员未指定或可能未正确指定的内容。由于核心指南针对新老 C++ 开发人员,这可能就是提到“优化”的原因。

constexprnoexcept状态可能会以不同方式影响生成代码:

  • constexpr构造函数确保从常量表达式产生的值调用构造函数也将产生常量表达式。这可以允许诸如非常static量值之类的东西实际上不需要构造函数调用(例如,不需要静态初始化开销或锁定)。注意:这适用于本身不能存在于constexpr上下文中的类型——只要constexpr构造函数的格式是正确的。

  • noexcept可能会生成更好的消费代码汇编,因为编译器可能会假设不会发生异常(因此不需要堆栈展开代码)。此外,用于检查的模板等实用程序std::is_nothrow_constructible...可能会生成更优化的代码路径。

外的是,default在类体定义编构造也使它们的定义可见给调用者-这可以更好的内联(同样,否则可以用于优化错过,机会)。


核心指南中的示例并没有很好地展示这些优化。但是,请考虑以下示例,该示例说明了可以从defaulting 中受益的实际示例:

class Foo {
    int a;
    std::unique_ptr<int> b;
public:
    Foo() : a{42}, b{nullptr}{}
};
Run Code Online (Sandbox Code Playgroud)

在此示例中,以下情况为真:

  • 物的施工Foo{}不是一个常量表达式
  • 建设Foo{} noexcept

对比一下:

class Foo {
    int a = 42;
    std::unique_ptr<int> b = nullptr;
public:
    Foo() = default;
};
Run Code Online (Sandbox Code Playgroud)

从表面上看,这似乎是一样的。但突然之间,现在发生了以下变化:

  • Foo{}is constexpr,因为std::unique_ptrstd::nullptr_t构造函数constexpr(即使std::unique_ptr不能在完整的常量表达式中使用)
  • Foo{}是一个noexcept表达式

您可以将生成的程序集与此Live Example. 注意这个defaultcase不需要任何初始化指令foo;相反,它只是通过编译器指令将值分配为常量(即使值不是常量)。

当然,也可以这样写:

class Foo {
    int a;
    std::unique_ptr<int> b;
public:
    constexpr Foo() noexcept :a{42}, b{nullptr};
};
Run Code Online (Sandbox Code Playgroud)

然而,这需要事先知道Foo能够constexprnoexcept。弄错了会导致问题。更糟糕的是,随着代码随着时间的推移而演变,constexpr/noexcept状态可能会变得不正确——这是defaulting 构造函数会发现的。

使用default还有一个额外的好处,即随着代码的发展,它可能会在可能的地方添加 constexpr/ noexcept- 例如当标准库添加更多constexpr支持时。最后一点对于作者来说每次代码更改时都需要手动处理。


琐碎

如果您取消使用类内成员初始值设定项,那么最后值得一提的一点是:除非由编译器生成(例如通过defaulted 构造函数),否则代码中无法实现平凡。

class Bar {
    int a;
public:
    Bar() = default; // Bar{} is trivial!
};
Run Code Online (Sandbox Code Playgroud)

Triviality 为潜在的优化提供了一个完全不同的方向,因为一个简单的默认构造函数不需要对编译器进行任何操作。这允许编译器在Bar{}发现对象稍后被覆盖时完全省略任何内容。


Jos*_*ose 1

我认为假设C.45引用常量(示例和执行)很重要:

例子,不好

class X1 { // BAD: doesn't use member initializers
    string s;
    int i; public:
    X1() :s{"default"}, i{1} { }
    // ... };
Run Code Online (Sandbox Code Playgroud)

例子

 class X2 {
    string s = "default";
    int i = 1; public:
    // use compiler-generated default constructor
    // ... };
Run Code Online (Sandbox Code Playgroud)

执行

(简单)默认构造函数应该做的不仅仅是用常量初始化成员变量。

考虑到这一点,更容易证明(通过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
    // ... };
Run Code Online (Sandbox Code Playgroud)

维护者如何知道 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
    // ... };
Run Code Online (Sandbox Code Playgroud)

替代方案:我们可以从构造函数的默认参数中获得部分好处,这在旧代码中并不罕见。然而,这种方式不太明确,会导致传递更多参数,并且当有多个构造函数时会重复:

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
    // ... };
Run Code Online (Sandbox Code Playgroud)

执行

(Simple) Every constructor should initialize every member variable (either explicitly, via a delegating ctor call or via default
Run Code Online (Sandbox Code Playgroud)

建造)。(简单)构造函数的默认参数表明类内初始值设定项可能更合适。

  • 你说得非常好。正如他们在 C48 中所写的那样“避免维护问题。它会产生最短、最高效的代码。” 这表明,在 C45 中,他们也使用“高效”作为“更容易维护代码”,而不是“为编译器提供更多机会”。请注意,这并没有回答“初始化器列表是否没有提供与类内初始化器相同的优化机会?”,即编译器优化是否存在差异?我开始相信不。 (2认同)