为什么将值初始化指定为不调用平凡的默认构造函数?

Bri*_*ian 8 c++ language-lawyer

对类型对象进行值初始化T意味着:

...

— 如果T是(可能是 cv 限定的)类类型,没有用户提供或删除的默认构造函数,则对象被零初始化并检查默认初始化的语义约束,并且如果T具有非平凡的默认构造函数,对象是默认初始化的;

我理解这里的意图:如果用户没有为 声明默认构造函数T,或者在第一次声明时显式默认了它,那么值初始化的零初始化传递将确保对象的某些直接成员(例如那些基本类型)不会留下不确定的值。

我不明白的是为什么第二遍被指定为“检查默认初始化的语义约束,如果T有一个非平凡的默认构造函数,则对象被默认初始化”。对我来说,这与只是说“对象是默认初始化的”相同(无论默认构造函数是否微不足道)。如果构造函数实际上是微不足道的,那么调用它应该与不调用它相同。标准不需要告诉编译器不要生成调用,因为在 as-if 规则下允许这样的优化,任何好的编译器都会这样做。

我错过了什么吗?在值初始化上下文中,调用一个平凡的默认构造函数与不调用它有什么不同吗?

Bri*_*ian 2

我无法找到我的问题的权威答案。不过,通过查看 C++03 标准和 CWG 问题列表,我有了一个粗略的想法。

让我们回顾一下 C++03 中的值初始化。它为类型定义T如下:

  • 如果T是具有用户声明构造函数的类类型,则调用默认构造函数;
  • 如果T是非联合类类型,没有用户声明的构造函数[这意味着编译器隐式声明了默认构造函数],则每个基类子对象和成员子对象都是值初始化的;
  • [...]

第二个要点确保标量类型的直接子对象是零初始化的,因此比简单地调用隐式默认构造函数“更强”。这就是值初始化背后的意图。

因此,C++03 中的措辞是有意义的,并且(无需做出任何特殊例外)会导致以下结果:当默认构造函数很简单时,实际上不会发生任何函数调用。

在 C++11 中,零初始化的定义已更改,因此它也会将填充位初始化为零 ( CWG 694 )。我的猜测是,围绕值初始化的措辞已更改,以保证在简单的默认构造函数的情况下,值初始化也能保证将填充位清零。因此,在 C++11 中,当T(没有用户提供的默认构造函数的类型)进行值初始化时,发生的情况比仅对T. 相反,整个T对象首先被零初始化(以确保填充为零),然后调用默认构造函数。

但同样,我们的问题是,为什么 C++11 在默认构造函数很简单的情况下会创建一个特殊的异常,从而阻止它被调用。进行措辞更改的论文N2762没有解释为什么要放入该异常,但我们现在可以看到它保留了 C++03 行为,其中根本不调用简单的默认构造函数。我的猜测是作者故意添加这样的措辞来保留这种行为,但他们的动机尚不清楚。

一种可能的动机是,一般来说,constexprC++11 中不存在简单的默认构造函数,除了空类的情况:它们会使标量成员未初始化,这是不允许的。因此,在值初始化期间省略对默认构造函数的调用使得可以对一般默认可构造的对象进行值初始化,这是值得允许的;请参见CWG 644。然而,没有迹象表明 N2762 的作者是否打算允许这样做。(旁白:有关 CWG 644 的后续开发,请参阅CWG 1452。)请注意,在 C++20 中,此类简单的默认构造函数变成了constexpr. 请参阅相关的 SO 问题。

我发现 N2762 的作者更有可能只是谨慎:换句话说,他们可能想保留 C++03 的行为,即不调用简单的默认构造函数,以防万一更改此行为会导致他们没有预料到的问题。(不过,它可能与性能没有任何关系;编译器可以优化对简单默认构造函数的调用。)

尽管如此,我们应该观察到 C++11 行为与 C++03 行为加上填充的零初始化并不完全相同。假设我们有这样的类型:

struct T {
    struct U { U() {} } u;
    int x;
};
Run Code Online (Sandbox Code Playgroud)

在C++03中,值初始化意味着T被U::U调用然后x被零初始化。在C++11中,这意味着整个T对象被零初始化,然后T::T被调用,进而调用U::U. 所以与C++03相比,C++11这里多了一个函数调用。因此,尽管作者尽了最大努力,但行为并不相同。据我所知,这种差异不会破坏任何代码。