【问题标题】:Why is a member not getting zero-initialized in this example?为什么在此示例中成员未进行零初始化?
【发布时间】:2019-05-30 10:18:42
【问题描述】:

这是专门针对 C++11 的:

#include <iostream>
struct A {
    A(){}
    int i;
};
struct B : public A {
    int j;
};
int main() {
    B b = {};
    std::cout << b.i << b.j << std::endl;
}

使用 g++ 8.2.1 编译:

$ g++ -std=c++11 -pedantic-errors -Wuninitialized -O2 a.cpp
a.cpp: In function ‘int main()’:
a.cpp:25:25: warning: ‘b.B::<anonymous>.A::i’ is used uninitialized in this function [-Wuninitialized]
     std::cout << b.i << " " << b.j << std::endl

gcc 将 b.i 检测为未初始化,但我认为它应该与 b.j 一起进行零初始化。

相信正在发生的事情(特别是 C++11,来自 ISO/IEC 工作草案 N3337,强调我的):

  • B 不是聚合,因为它有一个基类。公共基类只允许在 C++17 的聚合中使用。
  • A 不是聚合,因为它有一个用户提供的构造函数

第 8.5.1 节

聚合是一个数组或类(第 9 条),没有用户提供的构造函数 (12.1),非静态数据成员没有大括号或等号初始化器 (9.2),没有私有或受保护的非静态数据成员(第 11 条), 没有基类(第 10 条),也没有虚函数 (10.3)。

  • b 正在使用空的括号初始化列表初始化列表

第 8.5.4 节

类型 T 的对象或引用的列表初始化定义如下:
如果初始化列表没有元素并且 T 是具有默认构造函数的类类型,则该对象是值初始化的
— 否则,如果 T 是一个聚合,则执行聚合初始化 (8.5.1)。

  • 这意味着 b 得到值初始化
  • B 有一个隐式定义的默认构造函数,所以 b 值初始化调用零初始化
  • b.B::A 被零初始化,它对b.B::A.i 进行零初始化,然后b.B::j 被零初始化。

第 8.5 节

对 T 类型的对象或引用进行零初始化意味着:
...
— 如果 T 是(可能是 cv 限定的)非联合类类型,每个非静态数据成员和每个基类子对象都初始化为零,并且填充初始化为零位; p>

...

对 T 类型的对象进行值初始化意味着:
— 如果 T 是具有用户提供的构造函数(12.1)的(可能是 cv 限定的)类类型(第 9 条),则 调用 T 的默认构造函数(如果 T 没有可访问的默认值,则初始化格式错误 构造函数);
如果 T 是(可能是 cv 限定的)非联合类类型,没有用户提供的构造函数,则对象 是零初始化的,并且如果 T 的隐式声明的默认构造函数是非平凡的,则该构造函数是 调用。

但是,gcc 似乎只说 b.B::j 将被零初始化。为什么是这样?

我能想到的一个原因是,如果 B 被视为一个聚合,它将用一个空列表初始化 b.B::A。 不过,B 肯定不是聚合,因为如果我们尝试使用聚合初始化,gcc 会正确出错。

// ... as in the above example
int main() {
    B b = {A{}, 1};
    std::cout << b.i << " " << b.j << std::endl;
}

用 C++11 编译

$ g++ -std=c++11 -pedantic-errors -Wuninitialized -O2 a.cpp
a.cpp: In function ‘int main()’:
a.cpp:10:18: error: could not convert ‘{A(), 1}’ from ‘<brace-enclosed initializer list>’ to ‘B’
     B b = {A{}, 1};

用 C++17 编译

g++ -std=c++17 -pedantic-errors -Wuninitialized -O2 a.cpp
a.cpp: In function ‘int main()’:
a.cpp:11:25: warning: ‘b.B::<anonymous>.A::i’ is used uninitialized in this function [-Wuninitialized]
     std::cout << b.i << " " << b.j << std::endl;

我们可以看到b.i 未初始化,因为B 是一个聚合,而b.B::A 正在由一个本身使A::i 未初始化的表达式初始化。

所以它不是一个聚合。另一个原因是如果b.B::j 被零初始化,b.B::A 被值初始化,但我在规范中看不到任何地方。

最后一个原因是是否调用了旧版本的标准。 来自cppreference

2) 如果 T 是没有任何用户提供的构造函数的非联合类类型,则 T 的每个非静态数据成员和基类组件都是值初始化的; (直到 C++11)

在这种情况下,b.B::ib.B::A 都将进行值初始化,这将导致此行为,但标记为 "(直到 C++11)"

【问题讨论】:

  • 您定义了一个构造函数并且编译器正在使用它。 A() = default; 或者根本不定义它,因为您没有任何用户定义的类型成员。
  • @Koori b 正在初始化为零。这应该对所有基和非静态成员进行零初始化。除非它没有被零初始化......
  • 问题是编译器没有“零初始化”基础,它实际上是在调用它的默认构造函数。由于您为 A 定义了构造函数,B 的构造函数将调用您定义的 A 构造函数。您只需删除 A(){} 或将其更改为 A() = default;
  • @MikeLui 如果您想要一个包含来自标准的引用来支持它们的答案,您应该添加language-lawyer 标签。
  • 编译器错误。 (但无论如何,依赖这个可能是不必要的脆弱。)

标签: c++ c++11 initialization language-lawyer list-initialization


【解决方案1】:

对于任何类,如果有单个用户定义的构造函数,则必须使用它,并且A(){} 不会初始化i

【讨论】:

  • 总有一个构造函数。根据定义的方式(用户是否提供),该对象可能会事先被零初始化。我相信在这种情况下应该有零初始化。正如我在标准中看到的那样,基类中构造函数的存在并不排除零初始化。 C++11 工作草案 8.5:“对 T 类型的对象或引用进行零初始化意味着:...如果 T 是(可能是 cv 限定的)非联合类类型,则每个非静态数据成员并且每个基类子对象都初始化为零,填充初始化为零位;"
  • The section on zero initialization 似乎不同意这一点。
  • 添加了“用户定义”。
【解决方案2】:

我也会处理编译器错误。

  • 我想我们都同意b 被初始化值(8.5.4)
  • 使用

    值初始化 T 类型的对象意味着:
    — 如果 T 是(可能是 cv 限定的)非联合类类型没有用户提供的构造函数,那么对象是零初始化 并且,如果 T 隐含-声明的默认构造函数是不平凡的,该构造函数被调用。

    所以应该发生的是首先零初始化,然后可能会调用默认ctors

  • 并具有以下定义:

    对 T 类型的对象或引用进行零初始化意味着:
    — 如果 T 是(可能是 cv 限定的)非联合类类型,则每个非静态数据成员和每个基类子对象都初始化为零,并且填充初始化为零位;

因此应该发生以下情况:

  1. 用零填充sizeof(B)
  2. 调用子对象A的构造函数,它什么都不做。

我认为这是优化中的一个错误。比较-O0-O1 的输出:https://godbolt.org/z/20QBoR。如果没有优化,行为是正确的。另一方面,Clang 在两者中都是正确的:https://godbolt.org/z/7uhlIi

这个“错误”仍然存在于 GCC 中较新的标准标志中:https://godbolt.org/z/ivkE5K

但是我假设在 C++20 中 B 是一个“聚合”,因此行为成为标准。

【讨论】:

    【解决方案3】:

    没有初始化i。它不会自动发生。您需要在类中或在类构造函数的初始化列表中对其进行初始化。或者删除您的非平凡/用户定义的构造函数(或= default 它,这使它变得微不足道)。

    编译器正在使用您提供的构造函数,并且 ctor 确实初始化 i

    【讨论】:

    • 或者去掉用户定义的默认构造函数。
    猜你喜欢
    • 2021-12-16
    • 2013-10-14
    • 1970-01-01
    • 1970-01-01
    • 2020-02-25
    • 2018-11-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多