我在初始化对象的时候遇到了gcc和clang之间意想不到的差异,并怀疑有一个(或两个)bug。
1.设置1:
struct A {
A() {}
int x;
};
struct B : A {
int y;
};
int main() {
...
B b {}; // How should b.x be initialized?
...
}
gcc使B b2 {}
初始化A,clang使其默认初始化(不接触x):https://godbolt.org/z/8znhr41ro
现在,为了弄清谁是正确的,我们来探索一下标准。value-initialization子句说:
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 -“对象是零初始化的”。前面几段提到的zero-initialization子句定义了对基类的处理:
6将T类型的对象或引用初始化为零意味着:
(6.1)如果T是标量类型([basic.types.general]),则对象初始化为通过将整数文字0(零)转换为T获得的值;
(6.2)如果T是(可能是cv限定的)非联合类类型,则其填充位([basic.types.general])被初始化为0位,并且每个非静态数据成员、每个非虚拟基类子对象,以及如果对象不是基类子对象,则每个虚拟基类子对象被零初始化;
...
所以我认为gcc就在这里,这是一个clang错误。*
1.设置2 -注解掉B的int y
成员:
struct A {
A() {}
int x;
};
struct B : A {
// int y;
};
这应该与案例1相同,但gcc的行为发生了变化:https://godbolt.org/z/Pvnh556de。这里gcc和clang都默认初始化(而不是零初始化)A,我怀疑这可能是两者中的一个bug。
这些bug真的需要报告吗?还是我错过了什么?
- 顺便说一句,我对这里的标准感到不安。用户表达了他的意图,即当A被示例化时需要发生什么:什么都不做我认为这个意图应该贯彻到嵌入的A的子节点(成员或基地)。零初始化A甚至可能没有任何意义。但那是另一个故事我很乐意推迟到另一个场合...
2条答案
按热度按时间hgb9j2n61#
根据问题中的注解,我将假设C17或更高版本:
B b {};
在语法上是 direct-list-initialization by empty initializer list,list-initialization 指的是使用花括号初始化列表。此操作的规则在[dcl.init.list]中指定。B
是一个聚合类(C17起)。因此,任何列表初始化都在语义上导致 aggregate-initialization,而不是 value-initialization。与带空括号的初始化(value-initialization)(语法歧义使其无法用作声明初始化器)和不带初始化器的声明(default-initialization)相反。
假设在聚合初始化中没有聚合元素具有任何显式的初始化器,则每个元素都被初始化为
= {}
,即 copy-list-initialization 由一个空的初始化器列表。其结果是
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::A::x
进行零初始化。在这种情况下,所有编译器都需要在测试用例中打印0
。关于你的最后一点:即使成员不会按照上面的方法初始化,程序也无法观察到是否发生了零初始化,因为任何读取值的尝试都将是UB。因此,编译器仍然可以自由地进行零初始化,而不管是否遵循as-if规则。
关于以前的C版本,不要太详细:
在C11和C14中,
x
的零初始化是有或没有y
保证的,因为B
不是C14中的聚合类,因此{}
导致整个B
对象的 * 值初始化 *,这意味着缺少用户提供/删除的构造函数的零初始化,这递归地意味着包括x
在内的所有子字节的零初始化。(根据值初始化规则,这仍然(通常)后跟一个默认的构造函数调用,可以替换零初始化值。在C98和C03中,编译会失败,因为
B
不是聚合类,因此不允许使用{}
进行初始化。在C98和C03中,值初始化的规则也是不同的,无论如何都不会导致递归零初始化。然而,CWG 178和CWG 543将其更改为当前行为,according to cppreference也应该被视为针对C++98的缺陷报告(我没有官方参考)。
dxxyhpgq2#
对我来说,9.1.1显然适用于这里,因为我们在结构体A中有“用户提供的默认构造函数”。因此,clang的行为是严格符合的,GCC的行为至少不是不符合的。也就是说,虽然GCC实际上执行了zero-init,但你不能依赖它。