gcc中的零初始化与clang中的默认初始化?

q43xntqr  于 2023-11-19  发布在  其他
关注(0)|答案(2)|浏览(167)

我在初始化对象的时候遇到了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
现在我们来探究一下标准,看看谁是正确的。值初始化子句说:
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])被初始化为0位,并且每个非静态数据成员、每个非虚拟基类子对象,以及如果对象不是基类子对象,则每个虚拟基类子对象被零初始化;
...
所以我认为gcc就在这里,这是一个clang bug。*
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甚至可能没有任何意义。但这是一个不同的故事,我很高兴推迟到另一个场合。
dgiusagp

dgiusagp1#

根据问题中的注解,我将假设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)相反。
假设在aggregate-initialization中没有任何聚合元素有任何显式的初始化器,每个元素都像= {}一样初始化,即 copy-list-initialization 由空的初始化器列表初始化。
其结果是B::y是零初始化的(就像int y = {};一样),我不会详细介绍。
A * 不是 * 一个聚合类,因为它有一个用户提供的构造函数,因此使用= {}进行的初始化在[dcl.init.list]/3.5之前都符合列表初始化规则中的福尔斯,该规则规定子对象将被 * 值初始化
根据你的引号,因为A * 确实 * 有一个用户提供的默认构造函数,所以子对象是由(9.1.1)
default-initialized* 的。对类类型进行默认初始化并不意味着任何零初始化,而只是通过调用默认构造函数进行初始化,在你的例子中,这并不初始化B::A::x
因此,B::A::x具有不确定值。
删除B::y成员并不会改变这一点。但是,您确定x是否具有不确定值的方法是有缺陷的。尝试读取不确定的int会导致未定义的行为,编译器不必提供任何与之前存储在同一内存位置的值一致的值。
因此,两个编译器在所有情况下都正确运行。
如果你用圆括号初始化,例如。

B b = B();

字符串
那么整个B对象将被 value-initialized,这将意味着所有Bzero-initialization,这将递归地使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的缺陷报告(我没有官方参考)。

ncgqoxb0

ncgqoxb02#

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

相关问题