为什么值初始化被指定为不调用平凡的默认构造函数?
Why is value-initialization specified as not calling trivial default constructors?
To value-initialize an object of type T
means:
...
— if T
is a (possibly cv-qualified) class type without a user-provided or deleted default constructor, then the object is zero-initialized and the semantic constraints for default-initialization are checked, and if T
has a non-trivial default constructor, the object is default-initialized;
我理解这里的意图:如果用户没有为 T
声明默认构造函数,或者在它的第一个声明中明确默认了它,那么值初始化的零初始化传递将确保对象的某些直接成员(例如基本类型的成员)不会留下不确定的值。
我不明白的是为什么第二遍被指定为“默认初始化的语义约束被检查,如果T
有一个非平凡的默认构造函数,对象是默认的-初始化”。对我来说,这等同于说“对象是默认初始化的”(不管默认构造函数是否是微不足道的)。如果构造函数实际上是微不足道的,调用它应该和不调用它一样。标准不需要告诉编译器不要生成调用,因为在 as-if 规则下允许这样的优化,任何好的编译器都会这样做。
我错过了什么吗?在值初始化上下文中,调用普通默认构造函数与不调用它有什么不同吗?
我无法找到对我的问题的权威回答。不过看了C++03标准和CWG的issues列表,我心里大概有数了。
让我们回顾一下 C++03 中的值初始化。它为类型 T
定义如下:
- 如果
T
是具有用户声明构造函数的class类型,则调用默认构造函数;
- 如果
T
是没有用户声明的构造函数的非联合 class 类型 [这意味着编译器隐式声明了默认构造函数],则每个基 class 子对象并且成员子对象是值初始化的;
- [...]
第二个要点确保标量类型的直接子对象被零初始化,因此比简单地调用隐式默认构造函数“更强”。这就是值初始化背后的意图。
所以 C++03 中的措辞是有道理的,并且(不需要做任何特殊的例外)导致以下结果:当默认构造函数是微不足道的时候,实际上不会发生函数调用。
在 C++11 中,零初始化的定义已更改,因此它也将填充位初始化为零 (CWG 694)。我的猜测是,围绕值初始化的措辞已更改,以保证在普通默认构造函数的情况下,值初始化也将保证将填充位清零。所以在 C++11 中,当 T
(没有用户提供的默认构造函数的类型)被值初始化时,发生的事情比仅仅对 T
的所有直接子对象进行值初始化更强大。相反,整个 T
对象首先被零初始化(以确保填充被清零),然后调用默认构造函数。
但同样,我们的问题是,为什么 C++11 在默认构造函数微不足道的情况下开辟了一个特殊的例外,以防止调用它。 N2762 的论文对措辞进行了更改,但并未解释为什么会出现该异常,但我们现在可以看到它保留了 C++03 的行为,其中根本没有调用简单的默认构造函数。我的猜测是作者故意使用措辞来保留这种行为,但他们的动机尚不清楚。
一个可能的动机是,普通的默认构造函数通常不是 C++11 中的 constexpr
,除了空 classes 的情况:它们会使标量成员未初始化,这是不允许的。因此,在值初始化期间省略对默认构造函数的调用使得可以对默认可构造的普通对象进行值初始化,这是可取的;参见 CWG 644. However, there is no indication of whether the authors of N2762 intended to allow this. (Aside: for subsequent developments related to CWG 644, see CWG 1452.) Note that in C++20, such trivial default constructors became constexpr
. See 。
我发现 N2762 的作者更可能只是出于谨慎:换句话说,他们可能想保留 C++03 不调用普通默认构造函数的行为,以防万一更改这会导致问题他们没有预料到。 (不过,它可能与性能没有任何关系;编译器可以优化对普通默认构造函数的调用。)
不过,我们应该注意到 C++11 的行为与 C++03 的行为不完全相同,加上填充的零初始化。假设我们有这样的类型:
struct T {
struct U { U() {} } u;
int x;
};
在C++03中,T
的值初始化意味着调用U::U
然后x
被零初始化。在 C++11 中,这意味着整个 T
对象被零初始化,然后调用 T::T
,进而调用 U::U
。所以C++11相比C++03这里多了一个函数调用。因此,尽管作者尽了最大努力,行为还是不一样。据我所知,这种差异不会破坏任何代码。
To value-initialize an object of type
T
means:...
— if
T
is a (possibly cv-qualified) class type without a user-provided or deleted default constructor, then the object is zero-initialized and the semantic constraints for default-initialization are checked, and ifT
has a non-trivial default constructor, the object is default-initialized;
我理解这里的意图:如果用户没有为 T
声明默认构造函数,或者在它的第一个声明中明确默认了它,那么值初始化的零初始化传递将确保对象的某些直接成员(例如基本类型的成员)不会留下不确定的值。
我不明白的是为什么第二遍被指定为“默认初始化的语义约束被检查,如果T
有一个非平凡的默认构造函数,对象是默认的-初始化”。对我来说,这等同于说“对象是默认初始化的”(不管默认构造函数是否是微不足道的)。如果构造函数实际上是微不足道的,调用它应该和不调用它一样。标准不需要告诉编译器不要生成调用,因为在 as-if 规则下允许这样的优化,任何好的编译器都会这样做。
我错过了什么吗?在值初始化上下文中,调用普通默认构造函数与不调用它有什么不同吗?
我无法找到对我的问题的权威回答。不过看了C++03标准和CWG的issues列表,我心里大概有数了。
让我们回顾一下 C++03 中的值初始化。它为类型 T
定义如下:
- 如果
T
是具有用户声明构造函数的class类型,则调用默认构造函数; - 如果
T
是没有用户声明的构造函数的非联合 class 类型 [这意味着编译器隐式声明了默认构造函数],则每个基 class 子对象并且成员子对象是值初始化的; - [...]
第二个要点确保标量类型的直接子对象被零初始化,因此比简单地调用隐式默认构造函数“更强”。这就是值初始化背后的意图。
所以 C++03 中的措辞是有道理的,并且(不需要做任何特殊的例外)导致以下结果:当默认构造函数是微不足道的时候,实际上不会发生函数调用。
在 C++11 中,零初始化的定义已更改,因此它也将填充位初始化为零 (CWG 694)。我的猜测是,围绕值初始化的措辞已更改,以保证在普通默认构造函数的情况下,值初始化也将保证将填充位清零。所以在 C++11 中,当 T
(没有用户提供的默认构造函数的类型)被值初始化时,发生的事情比仅仅对 T
的所有直接子对象进行值初始化更强大。相反,整个 T
对象首先被零初始化(以确保填充被清零),然后调用默认构造函数。
但同样,我们的问题是,为什么 C++11 在默认构造函数微不足道的情况下开辟了一个特殊的例外,以防止调用它。 N2762 的论文对措辞进行了更改,但并未解释为什么会出现该异常,但我们现在可以看到它保留了 C++03 的行为,其中根本没有调用简单的默认构造函数。我的猜测是作者故意使用措辞来保留这种行为,但他们的动机尚不清楚。
一个可能的动机是,普通的默认构造函数通常不是 C++11 中的 constexpr
,除了空 classes 的情况:它们会使标量成员未初始化,这是不允许的。因此,在值初始化期间省略对默认构造函数的调用使得可以对默认可构造的普通对象进行值初始化,这是可取的;参见 CWG 644. However, there is no indication of whether the authors of N2762 intended to allow this. (Aside: for subsequent developments related to CWG 644, see CWG 1452.) Note that in C++20, such trivial default constructors became constexpr
. See
我发现 N2762 的作者更可能只是出于谨慎:换句话说,他们可能想保留 C++03 不调用普通默认构造函数的行为,以防万一更改这会导致问题他们没有预料到。 (不过,它可能与性能没有任何关系;编译器可以优化对普通默认构造函数的调用。)
不过,我们应该注意到 C++11 的行为与 C++03 的行为不完全相同,加上填充的零初始化。假设我们有这样的类型:
struct T {
struct U { U() {} } u;
int x;
};
在C++03中,T
的值初始化意味着调用U::U
然后x
被零初始化。在 C++11 中,这意味着整个 T
对象被零初始化,然后调用 T::T
,进而调用 U::U
。所以C++11相比C++03这里多了一个函数调用。因此,尽管作者尽了最大努力,行为还是不一样。据我所知,这种差异不会破坏任何代码。