我在初始化对象的时候遇到了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甚至可能没有任何意义。但这是一个不同的故事,我很高兴推迟到另一个场合。
2条答案
按热度按时间dgiusagp1#
根据问题中的注解,我将假设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)相反。
假设在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
对象将被 value-initialized,这将意味着所有B
的 zero-initialization,这将递归地使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的缺陷报告(我没有官方参考)。
ncgqoxb02#
对我来说,9.1.1显然适用于这里,因为我们在结构体A中有“用户提供的默认构造函数”。因此clang的行为是严格符合的,而GCC的行为至少不是不符合的。也就是说,尽管GCC实际上做了zero-init,但你不能依赖它。