C++:gcc中的zero-init vs clang中的default-init?

xa9qqrwz  于 2023-10-19  发布在  其他
关注(0)|答案(2)|浏览(111)

我在初始化对象的时候遇到了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甚至可能没有任何意义。但那是另一个故事我很乐意推迟到另一个场合...
hgb9j2n6

hgb9j2n61#

根据问题中的注解,我将假设C17或更高版本:
B b {};在语法上是 direct-list-initialization by empty initializer list,list-initialization 指的是使用花括号初始化列表。此操作的规则在[dcl.init.list]中指定。
B是一个聚合类(C
17起)。因此,任何列表初始化都在语义上导致 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();

则整个B对象将被 * 值初始化 *,这将意味着所有B的 * 零初始化 *,这将递归地对B::A::x进行零初始化。在这种情况下,所有编译器都需要在测试用例中打印0
关于你的最后一点:即使成员不会按照上面的方法初始化,程序也无法观察到是否发生了零初始化,因为任何读取值的尝试都将是UB。因此,编译器仍然可以自由地进行零初始化,而不管是否遵循as-if规则。
关于以前的C版本,不要太详细:
在C
11和C14中,x的零初始化是有或没有y保证的,因为B不是C14中的聚合类,因此{}导致整个B对象的 * 值初始化 *,这意味着缺少用户提供/删除的构造函数的零初始化,这递归地意味着包括x在内的所有子字节的零初始化。(根据值初始化规则,这仍然(通常)后跟一个默认的构造函数调用,可以替换零初始化值。
在C98和C03中,编译会失败,因为B不是聚合类,因此不允许使用{}进行初始化。
在C98和C03中,值初始化的规则也是不同的,无论如何都不会导致递归零初始化。然而,CWG 178CWG 543将其更改为当前行为,according to cppreference也应该被视为针对C++98的缺陷报告(我没有官方参考)。

dxxyhpgq

dxxyhpgq2#

对我来说,9.1.1显然适用于这里,因为我们在结构体A中有“用户提供的默认构造函数”。因此,clang的行为是严格符合的,GCC的行为至少不是不符合的。也就是说,虽然GCC实际上执行了zero-init,但你不能依赖它。

相关问题