【问题标题】:Does C++ standard guarantee the initialization of padding bytes to zero for non-static aggregate objects?C++ 标准是否保证非静态聚合对象的填充字节初始化为零?
【发布时间】:2022-02-03 22:17:40
【问题描述】:

C++ 是否支持允许我们将对象及其所有填充字段初始化为零的语言结构。我在 cppreference.com 中发现了一些关于 zero-initialization 的令人鼓舞的措辞,这表明在某些情况下,填充字节也会被清零。

引自 cppreference.com:zero-initialization

以下情况会进行零初始化:

  1. 作为非类类型和没有构造函数的值初始化类类型成员的值初始化序列的一部分,包括未提供初始化器的聚合元素的值初始化。

零初始化的效果是:

  • 如果 T 是标量类型,则对象的初始值是显式转换为 T 的整数常量零。
  • 如果 T 是非联合类类型,则所有基类和非静态数据成员都初始化为零,并且所有填充都初始化为零位。构造函数(如果有)将被忽略。
  • ...

您会在value-initializationaggregate-initializationlist-initialization 中找到对零初始化的引用。

我使用相当最新的 GCC 和 clang C++ 编译器进行了测试,它们的行为似乎有所不同。

坦率地说,我努力解析这些规则,特别是考虑到不同的编译器行为,我无法弄清楚如何正确解释这些规则。

参见代码here(至少需要 C++11)。结果如下:

给定:Foo

struct Foo
{
    char x;
    int y;
    char z;
};
Construct g++ clang++
Foo() x:[----][0x42][0x43][0x44],v: 0 x:[----][----][----][----],v: 0
y:[----][----][----][----],v: 0 y:[----][----][----][----],v: 0
z:[----][0x4A][0x4B][0x4C],v: 0 z:[----][----][----][----],v: 0
Foo{} x:[----][----][----][----],v: 0 x:[----][0x42][0x43][0x44],v: 0
y:[----][----][----][----],v: 0 y:[----][----][----][----],v: 0
z:[----][----][----][----],v: 0 z:[----][0x4A][0x4B][0x4C],v: 0

这里[----]代表一个包含所有位0的字节,[0x..]是垃圾值。

如您所见,编译器输出表明填充未初始化。 Foo()Foo{} 都是值初始化。此外,Foo{} 是一个聚合初始化,缺少初始化程序。为什么没有触发零初始化规则?为什么没有触发填充规则?

我已经明白依赖填充字节为零不是一个好主意,甚至可能是未定义的行为,但我认为这不是这个问题的重点。

  • 问题 1:标准是否提供了一种可靠地初始化填充字节的方法?
  • 问题 2:另见:does c initialize structure padding。是否适用?
  • 问题 3:这些编译器是否符合标准?
  • 问题 4:编译器明显不同的行为的解释是什么?

【问题讨论】:

  • 你为什么不给它加标签c++或者language-lawyer
  • 在您的代码中,您还专门使用了 C++20。如果您不打算询问特定语言版本,我建议删除所有特定于版本的标签。
  • 我相信零初始化仅适用于静态/线程存储持续时间对象。 Dynamic 和 Automatic 对象不会(默认情况下)将它们的填充清零)除非您明确将它们初始化为零,因为这是额外的运行时成本。
  • 为什么要关心填充初始化?如果你依赖于特定的 padding 值,为什么不让 padding 显式成员,这样你就可以依赖对成员的标准要求和保证?毕竟,初始化填充是浪费 CPU 周期,这违反了 C++ 不为你不使用的东西付费的原则。
  • 我会注意到您为编译器定义了-O3。只要行为上没有明显的差异,编译器几乎可以做任何事情。填充可观察。

标签: c++ gcc initialization language-lawyer zero-initialization


【解决方案1】:

只有当类对象被零初始化时,填充位才会被清零,如您的报价中所述。

对于自动存储持续时间对象,零初始化仅在对象进行值初始化并且具有未删除的隐式默认构造函数且没有其他用户提供的默认构造函数时才会发生。 [dcl.init.general]/8.1这里满足了这些条件。

值初始化应该始终使用() 初始化程序进行。 ([dcl.init.general]/16.4)

{} 作为初始化器也可能发生值初始化。但是,如果类是此处的聚合,则首选聚合初始化,这不会导致值初始化。 ([dcl.init.list]/3.4)

在 C++14 之前,CWG 1301 更改了聚合初始化优先于值初始化的偏好,这也可能适用于 C++11。在 C++11 之前,规则可能有所不同,我没有检查。


所以我会说 Clang 的行为正确,而 GCC 在 Foo() 上是错误的,同时为 Foo{} 做不必要的工作(尽管正如 @PeterCordes 所指出的,将包括填充在内的整个对象归零实际上更有效)。


请注意,我并不完全清楚检查非零初始化填充字节的值是否具有您正在做的明确定义的行为。

对于默认初始化的情况,读取成员具有未定义的行为,因为它的值将是不确定的。

我希望在new 可能初始化它们之前填充也应该具有不确定的值。在这种情况下,如果没有零初始化,则检查它们的值会导致未定义的行为。

【讨论】:

  • 似乎 gcc 和 clang 在 malloc (在 operator new 重载中)之后从 init 循环到构造函数进行常量传播,所以它实际上只做一个存储。评论一下,我们可以在 asm 中看到 GCC 或 clang 选择只进行 2 字节存储和 dword 存储,或者使用 dword + qword 存储将整个对象归零。 gcc.godbolt.org/z/9M5ox6KPc 有趣的是,他们选择相反:对于new Foo() gcc 执行 3 个独立的存储以避免填充,clang 执行 2 个覆盖整个对象。对于new Foo{},情况正好相反。
  • (所以 GCC 在Foo{} 上的“不必要的工作”实际上是更有效的方式,并且它们在这两种情况下都应该做的事情,即使在正确性不需要的情况下也应该这样做。通常有 2 个商店在现代 x86(和更小的代码大小)上优于 3,无论错位如何,除非它对页面拆分不利。但是 malloc 在其目标 x86-64 SysV 中返回 16 字节对齐的内存。对象不是一个xorps xmm0,xmm0 / movups 16 个字节,否则会更好。)
  • @PeterCordes 我并没有真正考虑过代码生成。我添加了一条关于您的 cmets 的注释。然而,在operator new 已经设置它们的特定情况下,标准允许 是否将Foo{} 的填充归零是另一个问题。我不认为它打算禁止它,但我不确定它是否明确规定。
  • @PeterCordes Foo() 的行为对于局部变量是相同的,但有趣的是 Clang 使用局部变量对 Foo{} 进行完全归零。这让我觉得 Clang 的开发人员可能确实认为,如果之前已经设置了内存,那么在这种情况下清除填充是不允许的,但也许这只是翻译过程如何工作的一些意外结果。 gcc.godbolt.org/z/vKf133jbh
  • @lenkite 有趣的是,第二个不合规示例的描述是错误的。 test arg{}; 不是我提到的缺陷报告后整个结构的值初始化。当然,如果编译器没有实现它,那么完全依赖值初始化是一个问题。幸运的是,页面上给出的合规解决方案并不依赖它。
猜你喜欢
  • 1970-01-01
  • 2016-10-05
  • 2021-04-05
  • 1970-01-01
  • 2021-11-22
  • 2015-01-22
  • 2018-01-17
  • 2020-07-18
  • 1970-01-01
相关资源
最近更新 更多