默认值、值和零初始化混乱



我对value-&default-&zero初始化感到非常困惑。 特别是当他们开始采用不同的标准C++03C++11(和C++14)时。

我在这里引用并试图扩展一个非常好的答案 Value-/Default-/Zero- Init C++98C++03以使其更加通用,因为如果有人可以帮助填补所需的空白,这将帮助很多用户,以便对何时发生的情况有一个很好的概述?

通过示例简要介绍完整的见解:

有时,new运算符返回的内存将被初始化,有时它不会取决于你正在更新的类型是POD(纯旧数据),还是包含POD成员并使用编译器生成的默认构造函数的类。

  • 在 C++1998中,有两种类型的初始化:初始化和默认初始化
  • 在 C++2003年,添加了第三种初始化类型,即值初始化
  • 在 C++2011/C++2014中,仅添加了列表初始化并且值/默认值/零初始化的规则发生了一些更改。

假设:

struct A { int m; };                     
struct B { ~B(); int m; };               
struct C { C() : m(){}; ~C(); int m; };  
struct D { D(){}; int m; };             
struct E { E() = default; int m;}; /** only possible in c++11/14 */  
struct F {F(); int m;};  F::F() = default; /** only possible in c++11/14 */

在 C++98 编译器中,应发生以下情况

  • new A- 不确定值(A为 POD)
  • new A()- 零初始化
  • new B- 默认构造(B::m未初始化,B非 POD)
  • new B()- 默认构造(B::m未初始化)
  • new C- 默认构造(C::m初始化为零,C为非 POD)
  • new C()- 默认构造(C::m初始化为零)
  • new D- 默认构造(D::m未初始化,D非 POD)
  • new D()-默认构造?(D::m未初始化)

在符合 C++03 标准的编译器中,事情应该是这样工作的:

  • new A- 不确定值(A为 POD)
  • new A()- 值初始化A,这是零初始化,因为它是一个 POD。
  • new B- 默认初始化(B::m未初始化,B非 POD)
  • new B()- 值初始化B初始化所有字段,因为它的默认 ctor 是编译器生成的,而不是用户定义的。
  • new C- 默认初始化C,这将调用默认 ctor。(C::m初始化为零,C为非 POD)
  • new C()- 值初始化C,这将调用默认的 ctor。 (C::m初始化为零)
  • new D- 默认构造(D::m未初始化,D非 POD)
  • new D()-值初始化 D?,它调用默认的 ctor(D::m未初始化)

斜体值和 ? 是不确定性,请帮助更正:-)

在符合 C++11 的编译器中,事情应该是这样工作的:

???(如果我从这里开始,请帮忙,无论如何都会出错)

在符合 C++14 标准的编译器中,事情应该是这样工作的:???(如果我从这里开始,请帮忙,无论如何都会出错)(根据答复起草)

  • new A- 默认初始化A,编译器通用 ctor,(未初始化A::m保留)(A是 POD)

  • new A()- 值初始化A,这是零初始化,因为 2.点在 [dcl.init]/8

  • new B- 默认初始化B,编译器通用ctor,(未初始化B::m保留)(B是非POD)

  • new B()- 值初始化B,零初始化所有字段,因为它的默认 ctor 是编译器生成的,而不是用户定义的。

  • new C- 默认初始化C,这将调用默认 ctor。(C::m初始化为零,C为非 POD)

  • new C()- 值初始化C,这将调用默认的 ctor。(C::m初始化为零)

  • new D- 默认初始化D(D::m未初始化,D非 POD)

  • new D()- 值初始化D,调用默认的 ctor(D::m未初始化)

  • new E- 默认初始化E,这将调用 Comp. Gen. CTOR。(E::m未初始化,E 非 POD)

  • new E()- 值初始化E,从[dcl.init]/8中的 2 点开始零初始化E

  • new F- 默认初始化F,这将调用 Comp. Gen. CTOR。(F::m未初始化,F非 POD)

  • new F()- 值初始化F,默认初始化[dcl.init]/8中的 1. 点以来F(F如果 ctor 函数是用户声明的,并且在其第一个声明中没有显式默认或删除,则它是用户提供的。链接)

C++14 指定了在 [expr.new]/17 中使用new创建的对象的初始化(C++11 中的 [expr.new]/15,当时注释不是注释而是规范文本):

创建类型为T的对象并初始化该对象的新表达式对象如下:

  • 如果省略新初始值设定项,则对象默认初始化(8.5)。[注意:如果没有初始化 执行时,对象具有不确定的值。— 尾注]
  • 否则,将根据 8.5 的初始化规则解释new-initializer以进行直接初始化

默认初始化在 [dcl.init]/7 中定义(C++11 中的/6,措辞本身具有相同的效果):

默认初始化类型T

的对象意味着:
  • 如果T是(可能符合 cv 条件的)类类型(条款 9),则调用T的默认构造函数 (12.1)(并且初始化 如果T没有默认构造函数或重载解析,则格式不正确 (13.3) 导致歧义或功能被删除或 无法从初始化上下文中访问);
  • 如果T是数组类型,则每个元素都是默认初始化的;
  • 否则,不执行初始化。

因此

  • new A仅导致调用A的默认构造函数,该构造函数不会初始化m。不确定的值。对于new B应该是相同的。
  • new A()根据 [dcl.init]/11 (C++11 中的/10) 进行解释:

    初始值设定项是一组空括号(即())的对象应进行值初始化。

    现在考虑 [dcl.init]/8(C++11† 中的/7):

    对类型为T的对象进行值初始化意味着:

    • 如果T是一个(可能符合 CV 条件的)类类型(条款 9),没有默认构造函数 (12.1) 或默认构造函数 用户提供或删除,则对象默认初始化;
    • 如果T是没有用户提供或删除的默认构造函数的(可能符合 CV 条件的)类类型,则对象为 零初始化和语义约束 检查默认初始化,如果 T 具有非平凡的默认值 构造函数,对象默认初始化;
    • 如果T是数组类型,则每个元素都是值初始化的;
    • 否则,对象初始化为零。

    因此new A()将零初始化m。这应该等同于AB.

  • new Cnew C()将再次默认初始化对象,因为最后一个引号的第一个项目符号点适用(C 有一个用户提供的默认构造函数!但是,很明显,现在m在这两种情况下都在构造函数中初始化。


† 好吧,这一段在 C++11 中的措辞略有不同,这不会改变结果:

对类型为T的对象进行值初始化意味着:

  • 如果T是(可能符合CV条件的)类类型(第9条),并且 用户提供的构造函数 (12.1),然后是T的默认构造函数 被调用(如果 T 无法访问,则初始化格式不正确 默认构造函数);
  • 如果T是(可能符合简历条件的)非工会 没有用户提供的构造函数的类类型,则对象为 零初始化,如果T隐式声明的默认构造函数 是非平凡的,该构造函数被调用。
  • 如果T是数组类型, 然后对每个元素进行值初始化;
  • 否则,对象为 零初始化。

以下答案扩展了答案 https://stackoverflow.com/a/620402/977038,可作为C++ 98 和 C++ 03 的参考

引用答案

  1. 在 C++1998 中,有两种类型的初始化:零和默认
  2. 在 C++2003 年,第 3 种类型的初始化,值初始化是 添加。

C++11(参考 n3242)

初始值设定项

8.5 初始值设定项 [dcl.init] 指定变量 POD 或非 POD 可以初始化为大括号或等于初始值设定项,可以是大括号初始化列表初始化子句,统称为大括号或等于初始值设定项或使用 (表达式列表)。在 C++11 之前,仅支持(表达式列表)初始值设定项子句,尽管初始值设定项子句比我们在 C++11 中受到的限制更大。在 C++11 中,初始值设定项子句现在支持除赋值表达式之外的大括号初始化列表,就像 C++03 中一样。以下语法总结了新支持的子句,其中粗体部分是 C++11 标准中新添加的。

Initializer: nbsp nbsp brace-or-equal-initializer nbsp  expression-list)
brace-or-equal-initializer: nbsp nbsp= initializer-clause  nbsp nbsp braced-init-list initializer-clause: nbsp nbspassignment-expression
 nbsp nbspbraced-init-list initializer-list





 nbsp nbspinitializer-clause

...opt
 nbsp nbspinitializer-list , initializer-clause ...opt**
braced-init-list:
 nbsp nbsp{initializer-list ,opt}  nbsp nbsp{ }

初始化

与 C++03 一样,C++11 仍然支持三种形式的初始化


注意

以粗体突出显示的部分已添加到 C++11 中,删除线的部分已从 C++11 中删除。

  1. 初始值设定项类型:8.5.5 [dcl.init] _zero-initialize_

在以下情况下执行

  • 具有静态或线程存储持续时间的对象初始化为零
  • 如果初始值设定项少于数组元素的数量,则每个未显式初始化的元素应为零初始化
  • 值初始化期间,如果 T 是没有用户提供的构造函数的非联合类类型(可能符合 cv 条件),则对象初始化为零。

将 T 类型的对象或引用初始化零意味着:

  • 如果 T 是标量类型 (3.9),则对象设置为值 0(零),作为整数常量表达式,转换为 T;
  • 如果 T 是(可能符合 cv 条件的)非联合类类型,则每个非静态数据成员和每个基类子对象初始化为零,填充初始化为零位;
  • 如果 T 是(可能符合 cv 条件的)联合类型,则对象的第一个非静态命名数据成员初始化为零,填充初始化为零位;
  • 如果 T 是数组类型,则每个元素初始化为零;
  • 如果 T 是引用类型,则不执行初始化。

阿拉伯数字。初始值设定项类型:8.5.6 [dcl.init] _default-initialize_

在以下情况下执行

  • 如果省略 new-initializer,则对象默认初始化;如果未执行初始化,则对象具有不确定的值。
  • 如果未为对象指定初始值设定项,则该对象默认初始化,但具有静态或线程存储持续时间的对象除外
  • 当构造函数
  • 初始值设定项列表中未提及基类或非静态数据成员并且调用该构造函数时。

默认初始化 T 类型的对象意味着:

  • 如果 T 是(可能符合 cv 条件的)非 POD类类型(条款 9),则调用 T 的默认构造函数(如果 T 没有可访问的默认构造函数,则初始化格式不正确);
  • 如果 T 是数组类型,则每个元素都是默认初始化的;
  • 否则,不执行初始化。

注意在 C++11 之前,当未使用初始值设定项时,只有具有自动存储持续时间的非 POD 类类型才被视为默认初始化。


3.初始值设定项类型:8.5.7 [dcl.init] _value-initialize_

  1. 当一个对象(无名称的临时、命名变量、动态存储持续时间或非静态数据成员)的初始值设定项是一组空的括号,即 () 或大括号 {}

对 T 类型的对象进行值初始化意味着:

  • 如果 T 是具有用户提供的构造函数 (12.1) 的(可能符合 cv 条件的)类类型(条款 9),则调用 T 的默认构造函数(如果 T 不可访问,则初始化格式不正确) 默认构造函数);
  • 如果 T 是没有用户提供的构造函数的(可能符合 cv 条件的)非联合类类型,则 T 的每个非静态数据成员和基类组件都是值初始化的;则对象为零初始化,如果 T 的隐式声明的默认构造函数是非平凡的,则调用该构造函数。
  • 如果 T 是数组类型,则每个元素都是值初始化的;
  • 否则,对象初始化为零。

所以总结一下

标准中的相关引文以粗体突出显示

new A :
  • 默认初始化 (保留 A::m 未初始化)
  • new A() :零初始化 A,因为初始化的候选值没有用户提供或删除的默认构造函数。如果 T 是没有用户提供的构造函数的(可能符合 cv 条件的)非联合类类型,则对象初始化为零,如果 T 的隐式声明的默认构造函数是非平凡的,则调用该构造函数。
  • 新 B:
  • 默认初始化(保留 B::m 未初始化)
  • new B() : 值初始化 B,将所有字段初始化为零;如果 T 是具有用户提供的构造函数 (12.1) 的类类型(可能符合 cv 条件的)类类型(条款 9),则调用 T 的默认构造函数
  • new C :默认初始化 C,调用默认 ctor。如果 T 是(可能符合 cv 条件的)类类型(条款 9),则调用 T 的默认构造函数, 此外,如果省略新初始值设定项,则对象默认初始化
  • new C() :值初始化 C,调用默认 ctor。如果 T 是具有用户提供的构造函数 (12.1) 的类类型(可能符合 cv 条件的)类类型(条款 9),则调用 T 的默认构造函数。此外,初始值设定项是一组空括号的对象,即 (),应进行值初始化

我可以确认,在 C++11 中,C++14 下的问题中提到的所有内容都是正确的,至少根据编译器实现。

为了验证这一点,我将以下代码添加到我的测试套件中。我在GCC 7.4.0,GCC 5.4.0,Clang 10.0.1和VS 2017中使用-std=c++11 -O3进行了测试,以下所有测试都通过了。

#include <gtest/gtest.h>
#include <memory>
struct A { int m;                    };
struct B { int m;            ~B(){}; };
struct C { int m; C():m(){}; ~C(){}; };
struct D { int m; D(){};             };
struct E { int m; E() = default;     };
struct F { int m; F();               }; F::F() = default;
// We use this macro to fill stack memory with something else than 0.
// Subsequent calls to EXPECT_NE(a.m, 0) are undefined behavior in theory, but
// pass in practice, and help illustrate that `a.m` is indeed not initialized
// to zero. Note that we initially tried the more aggressive test
// EXPECT_EQ(a.m, 42), but it didn't pass on all compilers (a.m wasn't equal to
// 42, but was still equal to some garbage value, not zero).
//
// Update 2020-12-14: Even the less aggressive EXPECT_NE(a.m, 0) fails in some
// machines, so we comment them out. But this change in behavior does illustrate
// that, in fact, the behavior was undefined.
//
#define FILL { int m = 42; EXPECT_EQ(m, 42); }
// We use this macro to fill heap memory with something else than 0, before
// doing a placement new at that same exact location. Subsequent calls to
// EXPECT_EQ(a->m, 42) are undefined behavior in theory, but pass in practice,
// and help illustrate that `a->m` is indeed not initialized to zero.
//
#define FILLH(b) std::unique_ptr<int> bp(new int(42)); int* b = bp.get(); EXPECT_EQ(*b, 42)
TEST(TestZero, StackDefaultInitialization)
{
//{ FILL; A a; EXPECT_NE(a.m, 0); } // UB!
//{ FILL; B a; EXPECT_NE(a.m, 0); } // UB!
{ FILL; C a; EXPECT_EQ(a.m, 0); }
//{ FILL; D a; EXPECT_NE(a.m, 0); } // UB!
//{ FILL; E a; EXPECT_NE(a.m, 0); } // UB!
//{ FILL; F a; EXPECT_NE(a.m, 0); } // UB!
}
TEST(TestZero, StackValueInitialization)
{
{ FILL; A a = A(); EXPECT_EQ(a.m, 0); }
{ FILL; B a = B(); EXPECT_EQ(a.m, 0); }
{ FILL; C a = C(); EXPECT_EQ(a.m, 0); }
//{ FILL; D a = D(); EXPECT_NE(a.m, 0); } // UB!
{ FILL; E a = E(); EXPECT_EQ(a.m, 0); }
//{ FILL; F a = F(); EXPECT_NE(a.m, 0); } // UB!
}
TEST(TestZero, StackListInitialization)
{
{ FILL; A a{}; EXPECT_EQ(a.m, 0); }
{ FILL; B a{}; EXPECT_EQ(a.m, 0); }
{ FILL; C a{}; EXPECT_EQ(a.m, 0); }
//{ FILL; D a{}; EXPECT_NE(a.m, 0); } // UB!
{ FILL; E a{}; EXPECT_EQ(a.m, 0); }
//{ FILL; F a{}; EXPECT_NE(a.m, 0); } // UB!
}
TEST(TestZero, HeapDefaultInitialization)
{
{ FILLH(b); A* a = new (b) A; EXPECT_EQ(a->m, 42); } // ~UB
{ FILLH(b); B* a = new (b) B; EXPECT_EQ(a->m, 42); } // ~UB
{ FILLH(b); C* a = new (b) C; EXPECT_EQ(a->m, 0);  }
{ FILLH(b); D* a = new (b) D; EXPECT_EQ(a->m, 42); } // ~UB
{ FILLH(b); E* a = new (b) E; EXPECT_EQ(a->m, 42); } // ~UB
{ FILLH(b); F* a = new (b) F; EXPECT_EQ(a->m, 42); } // ~UB
}
TEST(TestZero, HeapValueInitialization)
{
{ FILLH(b); A* a = new (b) A(); EXPECT_EQ(a->m, 0);  }
{ FILLH(b); B* a = new (b) B(); EXPECT_EQ(a->m, 0);  }
{ FILLH(b); C* a = new (b) C(); EXPECT_EQ(a->m, 0);  }
{ FILLH(b); D* a = new (b) D(); EXPECT_EQ(a->m, 42); } // ~UB
{ FILLH(b); E* a = new (b) E(); EXPECT_EQ(a->m, 0);  }
{ FILLH(b); F* a = new (b) F(); EXPECT_EQ(a->m, 42); } // ~UB
}
TEST(TestZero, HeapListInitialization)
{
{ FILLH(b); A* a = new (b) A{}; EXPECT_EQ(a->m, 0);  }
{ FILLH(b); B* a = new (b) B{}; EXPECT_EQ(a->m, 0);  }
{ FILLH(b); C* a = new (b) C{}; EXPECT_EQ(a->m, 0);  }
{ FILLH(b); D* a = new (b) D{}; EXPECT_EQ(a->m, 42); } // ~UB
{ FILLH(b); E* a = new (b) E{}; EXPECT_EQ(a->m, 0);  }
{ FILLH(b); F* a = new (b) F{}; EXPECT_EQ(a->m, 42); } // ~UB
}
int main(int argc, char **argv)
{
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}

提到UB!的地方是未定义的行为,实际行为可能取决于许多因素(a.m可能等于 42、0 或其他一些垃圾)。提到~UB的地方在理论上也是未定义的行为,但在实践中,由于使用了新放置,因此a->m不太可能等于 42 以外的任何东西。

最新更新