默认、值和零初始化混乱

Default, value and zero initialization mess

我对值初始化、默认初始化和零初始化感到很困惑。 尤其是当他们针对不同的标准 C++03C++11 (以及 C++14 ).

我在这里引用并尝试扩展一个非常好的答案 Value-/Default-/Zero- Init C++98 and C++03 以使其更通用,因为如果有人可以帮助填补所需的空白以更好地概述什么时候会发生什么?

通过示例概括的完整见解:

有时 new 运算符返回的内存会被初始化,有时不会,这取决于您要更新的类型是 POD (plain old data),还是 class包含 POD 成员并使用编译器生成的默认构造函数。

假设:

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 编译器中,应出现以下情况

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

斜体值和 ?有不明之处,请帮忙改正:-)

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

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

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

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

A new-expression that creates an object of type T initializes that object as follows:

  • If the new-initializer is omitted, the object is default-initialized (8.5). [ Note: If no initialization is performed, the object has an indeterminate value. — end note ]
  • Otherwise, the new-initializer is interpreted according to the initialization rules of 8.5 for direct-initialization.

default-initialization定义在[dcl.init]/7(C++11中的/6,写法本身也有同样的作用):

To default-initialize an object of type T means:

  • if T is a (possibly cv-qualified) class type (Clause 9), the default constructor (12.1) for T is called (and the initialization is ill-formed if T has no default constructor or overload resolution (13.3) results in an ambiguity or in a function that is deleted or inaccessible from the context of the initialization);
  • if T is an array type, each element is default-initialized;
  • otherwise, no initialization is performed.

因此

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

    An object whose initializer is an empty set of parentheses, i.e., (), shall be value-initialized.

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

    To value-initialize an object of type T means:

    • if T is a (possibly cv-qualified) class type (Clause 9) with either no default constructor (12.1) or a default constructor that is user-provided or deleted, then the object is default-initialized;
    • if T is a (possibly cv-qualified) class type without a user-provided or deleted default constructor, then the object is zero-initialized and the semantic constraints for default-initialization are checked, and if T has a non-trivial default constructor, the object is default-initialized;
    • if T is an array type, then each element is value-initialized;
    • otherwise, the object is zero-initialized.

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

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


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

To value-initialize an object of type T means:

  • if T is a (possibly cv-qualified) class type (Clause 9) with a user-provided constructor (12.1), then the default constructor for T is called (and the initialization is ill-formed if T has no accessible default constructor);
  • if T is a (possibly cv-qualified) non-union class type without a user-provided constructor, then the object is zero-initialized and, if T’s implicitly-declared default constructor is non-trivial, that constructor is called.
  • if T is an array type, then each element is value-initialized;
  • otherwise, the object is zero-initialized.

以下答案扩展了答案,可作为 C++ 98 和 C++ 03 的参考

引用答案

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

C++11(参考n3242)

初始化器

8.5 Initializers [dcl.init] 指定变量 POD 或非 POD 可以初始化为 brace-or-equal-initializer 可以是 braced-init-listinitializer-clause 统称为 brace-or-equal-initializer 或使用(表达式列表)。在 C++11 之前,尽管 initializer-clause[=] 仅支持 (expression-list)initializer-clause 124=] 比我们在 C++11 中的限制更多。在 C++11 中,initializer-clause 除了 assignment-expression 现在还支持 braced-init-list ] 就像在 C++03 中一样。以下语法总结了新支持的子句,粗体部分为C++11标准新增。

初始值设定项:
    大括号或相等初始化器
    (表达式列表)
大括号或相等初始化器:
    = 初始化子句
    braced-init-list
初始化子句:
    assignment-表达式
    braced-init-list
初始化程序列表:
    initializer-clause ...opt
    初始化器列表,初始化器子句 ...opt**
支撑初始化列表:
    { 初始化列表 ,opt }
    { }

初始化

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


备注

The part highlighted in bold has been added in C++11 and the one that is striked out has been removed from C++11.

  1. 初始化程序 Type:8.5.5 [dcl.init] _零初始化_

在以下情况下执行

  • 具有静态或线程存储持续时间的对象被零初始化
  • 如果初始化器的数量少于数组元素的数量,则每个未显式初始化的元素都应进行零初始化
  • value-initialize 期间,如果 T 是一个(可能是 cv 限定的)非联合 class 类型,没有用户提供的构造函数,则对象是零初始化。

To zero-initialize an object or reference of type T means:

  • if T is a scalar type (3.9), the object is set to the value 0 (zero), taken as an integral constant expression, converted to T;
  • if T is a (possibly cv-qualified) non-union class type, each non-static data member and each base-class subobject is zero-initialized and padding is initialized to zero bits;
  • if T is a (possibly cv-qualified) union type, the object’s first non-static named data member is zero initialized and padding is initialized to zero bits;
  • if T is an array type, each element is zero-initialized;
  • if T is a reference type, no initialization is performed.

2。初始化程序类型:8.5.6 [dcl.init] _default-initialize_

在以下情况下执行

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

To default-initialize an object of type T means:

  • if T is a (possibly cv-qualified) non-POD class type (Clause 9), the default constructor for T is called (and the initialization is ill-formed if T has no accessible default constructor);
  • if T is an array type, each element is default-initialized;
  • otherwise, no initialization is performed.

Note Until C++11, only non-POD class types with automatic storage duration were considered to be default-initialized when no initializer is used.


3。初始化程序类型:8.5.7 [dcl.init] _value-initialize_

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

To value-initialize an object of type T means:

  • if T is a (possibly cv-qualified) class type (Clause 9) with a user-provided constructor (12.1), then the default constructor for T is called (and the initialization is ill-formed if T has no accessible default constructor);
  • if T is a (possibly cv-qualified) non-union class type without a user-provided constructor, then every non-static data member and base-class component of T is value-initialized; then the object is zero-initialized and, if T’s implicitly-declared default constructor is non-trivial, that constructor is called.
  • if T is an array type, then each element is value-initialized;
  • otherwise, the object is zero-initialized.

总结一下

Note The relevant quotation from the standard is highlighted in bold

  • 新 A : 默认初始化(留下 A::m 未初始化)
  • new A() : 零初始化 A,因为值初始化候选没有用户提供或删除的默认构造函数。 如果 T 是一个(可能是 cv 限定的)非联合 class 类型,没有用户提供的构造函数,那么对象是零初始化的,如果 T 是隐式声明的默认构造函数是非常重要的,调用该构造函数。
  • 新 B : 默认初始化(留下 B::m 未初始化)
  • new B() : 对 B 进行值初始化,对所有字段进行零初始化; 如果 T 是(可能是 cv 限定的)class 类型(第 9 条),具有用户提供的构造函数 (12.1),则调用 T 的默认构造函数
  • new C : 默认初始化 C,调用默认构造函数。 如果 T 是(可能是 cv 限定的)class 类型(第 9 条),则调用 T 的默认构造函数,此外如果省略 new-initializer,对象默认初始化
  • new C() : 值初始化 C,调用默认构造函数。 如果 T 是(可能是 cv 限定的)class 类型(第 9 条),具有用户提供的构造函数 (12.1),则调用 T 的默认构造函数。此外,初始化器为空括号集的对象,即(),应被值初始化

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

为了验证这一点,我在我的 test suite 中添加了以下代码。我在 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.