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

Ofe*_*lon 6 c++ gcc clang language-lawyer

我在值初始化对象时遇到了 gcc 和 clang 之间意想不到的差异,并怀疑存在一个(或两个)错误。

  1. 设置1:
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。*

  1. 设置 2 - 注释掉 B 的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,我怀疑这可能是两者的错误。

这些确实有需要报告的错误吗?或者我错过了什么?


  • 顺便说一句,我对这里的标准感到不安。用户表达了他们对实例化 A 时需要发生的事情的意图:“什么都不做”。我想说这个意图应该贯彻到 A 作为子对象(成员或基础)嵌入的情况。将 A 零初始化甚至可能没有任何意义。但这是一个不同的故事,我很乐意推迟到另一个场合。

use*_*522 7

我将根据问题中的注释假设 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 178CWG 543更改为当前行为,根据 cppreference也应被视为针对 C++98 的缺陷报告(我对此没有官方参考)。

  • @OfekShilon 自 C++17 以来情况并非如此。现在允许聚合类使用基类。这就是我向您询问 C++ 版本的原因。 (5认同)
  • 我不知道零初始化甚至可以通过用户提供的构造函数传播。我可能有十几个关于这个问题的答案都是错误的...... (4认同)