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

时间:2019-05-23 09:44:15

标签: c++ initialization default-constructor cpp-core-guidelines

this answer触发,我正在读core guidelines

  

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

给出的理由是

  

原因

     

使用类内成员初始化程序,编译器可以生成   为您服务。编译器生成的函数可以更多   

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

“不良”示例是:

Example, bad

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

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

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

2 个答案:

答案 0 :(得分:9)

简答

如果作者包含正确的 defaultconstexpr 状态,noexcepted 构造函数应该具有与等效初始化构造函数相同的生成程序集。< /p>

我怀疑“可以更高效”指的是这样一个事实:一般来说,它会生成比同等的开发人员编写的代码更优化的代码,这些代码会错失机会,例如 inline、{{1} },和constexpr

长答案

noexcepted 构造函数执行的一个重要功能是它们解释并推导出 defaultconstexpr 的正确状态

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

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

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

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

除此之外,在类主体中定义的 std::is_nothrow_constructible...ed 构造函数也使它们的定义对调用者可见——这允许更好的内联(同样,否则可能会错过优化的机会).


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

中受益的实际示例
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{},因为 constexprstd::nullptr_t constructorstd::unique_ptr(即使 constexpr 不能用于完整的常量表达式)
  • std::unique_ptr 是一个 Foo{} 表达式

您可以将生成的程序集与此 Live Example 进行比较。请注意,noexcept 情况不需要任何指令来初始化 default;相反,它只是通过编译器指令将值分配为常量(即使值不是常量)。

当然也可以这样写:

foo

然而,这需要先验知识class Foo { int a; std::unique_ptr<int> b; public: constexpr Foo() noexcept :a{42}, b{nullptr}; }; 能够同时是Fooconstexpr。弄错了会导致问题。更糟糕的是,随着代码的发展,noexcept/constexpr 状态可能会变得不正确 -- 而noexcept构造函数会发现这种情况。

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


琐碎

如果不使用类内成员初始化器,那么最后值得一提的一点是:代码中没有办法实现平凡,除非它是由编译器生成的(例如通过 constexpred构造函数)。

default

Triviality 为潜在的优化提供了一个完全不同的方向,因为一个简单的默认构造函数不需要对编译器进行任何操作。这允许编译器在发现对象稍后被覆盖时完全省略任何 class Bar { int a; public: Bar() = default; // Bar{} is trivial! };

答案 1 :(得分:1)

我认为假设 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
    // ... };

执法

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

考虑到这一点,更容易证明(通过 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

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