Ofe*_*lon 6 c++ gcc clang language-lawyer
我在值初始化对象时遇到了 gcc 和 clang 之间意想不到的差异,并怀疑存在一个(或两个)错误。
struct A {
A() {}
int x;
};
struct B : A {
int y;
};
int main() {
...
B b {}; // How should b.x be initialized?
...
}
Run Code Online (Sandbox Code Playgroud)
gcc 使B b2 {}
A 为零初始化,clang 使其默认初始化(不触及 x): https: //godbolt.org/z/8znhr41ro
现在我们要深入探讨标准,以了解谁是对的。值初始化子句说:
9 对 T 类型的对象进行值初始化意味着:
(9.1) 如果 T 是一个(可能是 cv 限定的)类类型 ([class]),那么
(9.1.1) 如果 T 没有默认构造函数 ([class.default.ctor]) 或者用户提供或删除的默认构造函数,则该对象被默认初始化;
(9.1.2) 否则,该对象被零初始化,并且检查默认初始化的语义约束,并且如果 T 具有非平凡的默认构造函数,则该对象被默认初始化;
(9.2) 如果 T 是数组类型,则每个元素都是值初始化的;
(9.3) 否则,该对象被零初始化。
虽然 9.1.2 的措辞相当糟糕,但我认为与此代码相关的项目是 9.3 - “对象零初始化”。前面几段的零初始化子句确实定义了基类的处理:
6 对 T 类型的对象或引用进行零初始化意味着:
(6.1) 如果 T 是标量类型 ([basic.types.general]),则该对象被初始化为通过将整数文字 0(零)转换为 T 获得的值;
(6.2) 如果 T 是一个(可能是 cv 限定的)非联合类类型,则其填充位 ([basic.types.general]) 被初始化为零位,并且每个非静态数据成员、每个非虚拟基类子对象,并且,如果该对象不是基类子对象,则每个虚拟基类子对象都被零初始化;
...
所以我认为 gcc 就在这里,这是一个 clang bug。*
int y
成员:struct A {
A() {}
int x;
};
struct B : A {
// int y;
};
Run Code Online (Sandbox Code Playgroud)
这应该与情况 1 相同,但 gcc 的行为发生了变化: https: //godbolt.org/z/Pvnh556de。这里 gcc 和 clang 默认初始化(而不是零初始化)A,我怀疑这可能是两者的错误。
这些确实有需要报告的错误吗?或者我错过了什么?
我将根据问题中的注释假设 C++17 或更高版本:
B b {};
从语法上讲,是通过空初始值设定项列表进行直接列表初始化,列表初始化是指使用大括号初始值设定项列表。其规则在 [dcl.init.list] 中指定。
B
是聚合类 (C++17 起)。因此,任何列表初始化在语义上都会导致聚合初始化,而不是值初始化。
与带有空括号的初始化相反,空括号的初始化将是值初始化(语法歧义使其无法用作声明初始化器),而没有初始化器的声明将是默认初始化。
假设聚合初始化中没有任何聚合元素具有任何显式初始化程序,则每个元素都像通过 一样进行初始化= {}
,即通过空初始化程序列表进行复制列表初始化。
结果是B::y
零初始化(如int y = {};
),我不会详细介绍。
A
不是一个聚合类,因为它有一个用户提供的构造函数,因此初始化会落在列表初始化规则中,直到[dcl.init.list]/3.5声明子对象将被值初始化。= {}
根据您的引用,因为A
确实有一个用户提供的默认构造函数,所以子对象由 (9.1.1)默认初始化。默认初始化类类型并不意味着任何零初始化,而只是通过调用默认构造函数进行初始化,在您的情况下,默认构造函数不会初始化B::A::x
。
所以,B::A::x
有一个不确定的值。
删除该B::y
成员不会改变任何事情。但是,您确定是否x
具有不确定值的方法是有缺陷的。尝试读取不确定int
会导致未定义的行为,并且编译器不必提供与之前存储在同一内存位置的值一致的任何值。
所以两个编译器在所有情况下都表现正确。
您是否用括号初始化,例如
B b = B();
Run Code Online (Sandbox Code Playgroud)
那么整个B
对象将被值初始化,这将意味着所有 的零初始化B
,这将递归地零初始化B::A::x
。在这种情况下,所有编译器都需要0
在您的测试用例中打印。
关于你的最后一点:即使成员不会按照上述初始化,程序也无法观察到是否发生了零初始化,因为任何读取该值的尝试都将是 UB。因此,无论在 as-if 规则下,编译器仍然可以自由地进行零初始化。
关于以前的 C++ 版本,无需赘述太多细节:
x
在 C++11 和 C++14 中,有或没有保证 的零初始化,y
因为B
它不是 C++14 中的聚合类,{}
因此会导致整个对象的值初始化B
,这意味着由于缺少用户提供/删除的构造函数,它递归地意味着所有子对象的零初始化,包括x
. (根据值初始化规则,(通常)后面仍然是默认构造函数调用,该调用可能会替换零初始化值。)
在 C++98 和 C++03 中,编译将失败,因为B
不是聚合类,因此{}
不允许使用语法进行初始化。
在 C++98 和 C++03 中,值初始化的规则也不同,并且无论如何都不会导致递归零初始化。然而,这已被CWG 178和CWG 543更改为当前行为,根据 cppreference也应被视为针对 C++98 的缺陷报告(我对此没有官方参考)。
归档时间: |
|
查看次数: |
271 次 |
最近记录: |