默认值和零初始化混乱

我对值-& default-& zero 初始化非常困惑。 特别是当它们适用于不同的标准 C + + 03C + + 11(以及 C + + 14)时。

我引用并试图扩展一个真正好的答案 Value-/Default-/Zero-Init < em > C + + 98 和 < em > C + + 03 在这里,使它更一般,因为它将帮助很多用户,如果有人可以帮助填补所需的差距,以有一个好的概述什么时候发生?

简而言之,通过例子得出的完整见解:

有时候 new 操作符返回的内存会被初始化,有时候不会取决于你新建的类型是否是 POD (普通的旧数据),或者是否是一个包含 POD 成员并且使用编译器生成的缺省构造函数的类。

  • C + + 1998中有两种初始化类型: 默认初始化
  • 在第三种初始化类型 C + + 2003年中,添加了 值初始化
  • C + + 2011/C + + 2014中只添加了 列表初始化,而 Value-/default-/zero-初始化的规则略有改变。

假设:

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()-0-初始化
  • 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)
  • 值-初始化 A,因为它是一个 POD,所以它是零初始化。
  • 默认初始化(留下未初始化的 B::mB是非 POD)
  • 值-初始化 B,它对所有字段进行零初始化,因为它的默认 ctor 是编译器生成的,而不是用户定义的。
  • 默认初始化 C,它调用默认的 ctor (C::m为零初始化,C为非 POD)
  • 值-初始化 C,它调用默认的 ctor
  • new D-缺省构造(D::m未初始化,D非 POD)
  • new D()-值初始化 D?,它调用默认的 ctor (D::m未初始化)

斜体值和? 是不确定的,请帮助纠正这一点: -)

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

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

在一个符合 C + + 14的编译器中,事情应该是这样的: (请帮助,如果我开始在这里,它无论如何都会出错) (草案基于答案)

  • new A-default-初始化 A,编译器 gen.ctor,(使 A::m未初始化)(A是 POD)

  • 值-初始化 A,这是从 < em > [ dcl.init ]/8中的2. point 开始的零初始化

  • new B-default-初始化 B,编译器 gen.ctor,(使 B::m未初始化)(B是非 POD)

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

  • 默认初始化 C,它调用默认的 ctor (C::m为零初始化,C为非 POD)

  • 值-初始化 C,它调用默认的 ctor

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

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

  • new E-default-初始化 E,它调用 comp.gen.ctor (E::m未初始化,E 为非 POD)

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

  • new F-default-初始化 F,它调用 comp.gen.ctor (F::m未初始化,F非 POD)

  • 值-初始化 F,从1开始 默认初始化 F。点(如果 F ctor 函数是用户声明的,并且在第一次声明时没有显式默认或删除,那么它是用户提供的。林克)

16456 次浏览

C++14 specifies initialization of objects created with new in [expr.new]/17 ([expr.new]/15 in C++11, and the note wasn't a note but normative text back then):

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 is defined in [dcl.init]/7 (/6 in C++11, and the wording itself has the same effect):

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.

Thus

  • new A solely causes As default constructor to be called, which does not initialize m. Indeterminate value. Should be the same for new B.
  • new A() is interpreted according to [dcl.init]/11 (/10 in C++11):

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

    And now consider [dcl.init]/8 (/7 in C++11†):

    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.

    Hence new A() will zero-initialize m. And this should be equivalent for A and B.

  • new C and new C() will default-initialize the object again, since the first bullet point from the last quote applies (C has a user-provided default constructor!). But, clearly, now m is initialized in the constructor in both cases.


† Well, this paragraph has slightly different wording in C++11, which does not alter the result:

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.

The following answer extends the answer https://stackoverflow.com/a/620402/977038 which would serve as a reference for C++ 98 and C++ 03

Quoting the answer

  1. In C++1998 there are 2 types of initialization: zero and default
  2. In C++2003 a 3rd type of initialization, value initialization was added.

C++11 (In reference to n3242)

Initializers

8.5 Initializers [dcl.init] specifies that a variable POD or non POD can be initialized either as brace-or-equal-initializer which can either be braced-init-list or initializer-clause aggregately referred to as brace-or-equal-initializer or using ( expression-list ). Previous to C++11, only (expression-list) or initializer-clause was supported though initializer-clause was more restricted then what we have in C++11. In C++11, initializer-clause now supports braced-init-list apart from braced-init-list0 as was in C++03. The following grammar summarizes the new supported clause, where the part is bold is newly added in the C++11 standard.

initializer:
    brace-or-equal-initializer
    ( expression-list )
brace-or-equal-initializer:
    = initializer-clause
    braced-init-list
initializer-clause:
    assignment-expression
    braced-init-list
initializer-list:
    initializer-clause ...opt
    initializer-list , initializer-clause ...opt**
braced-init-list:
    { initializer-list ,opt }
    { }

Initialization

Like C++03, C++11 still supports three form of initialize


Note

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. Initializer Type:8.5.5 [dcl.init] _zero-initialize_

Performed in the following cases

  • Objects with static or thread storage duration are zero-initialized
  • If there are fewer initializers than there are array elements, each element not explicitly initialized shall be zero-initialized
  • During value-initialize, if T is a (possibly cv-qualified) non-union class type without a user-provided constructor, then the object is zero-initialized.

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. Initializer Type: 8.5.6 [dcl.init] _default-initialize_

Performed in the following cases

  • If the new-initializer is omitted, the object is default-initialized; if no initialization is performed, the object has indeterminate value.
  • If no initializer is specified for an object, the object is default-initialized, except for Objects with static or thread storage duration
  • When a base class or a non-static data member is not mentioned in a constructor initializer list and that constructor is called.

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. Initializer Type: 8.5.7 [dcl.init] _value-initialize_

  1. When an object(nameless temporary, named variable, dynamic storage duration or non-static data member) whose initializer is an empty set of parentheses, i.e., () or braces {}

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.

So to summarize

Note The relevant quotation from the standard is highlighted in bold

  • new A : default-initializes (leaves A::m uninitialized)
  • new A() : Zero-initialize A, as the value initialized candidate does not have a user-provided or deleted 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.
  • new B : default-initializes (leaves B::m uninitialized)
  • new B() : value-initializes B which zero-initializes all fields; 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
  • new C : default-initializes C, which calls the default ctor. if T is a (possibly cv-qualified) class type (Clause 9), the default constructor for T is called, Moreover If the new-initializer is omitted, the object is default-initialized
  • new C() : value-initializes C, which calls the default ctor. 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. Moreover, An object whose initializer is an empty set of parentheses, i.e., (), shall be value-initialized

I can confirm that in C++11, everything mentioned in the question under C++14 is correct, at least as per compiler implementations.

In order to verify this, I added the following code to my test suite. I tested with -std=c++11 -O3 in GCC 7.4.0, GCC 5.4.0, Clang 10.0.1, and VS 2017, and all the tests below pass.

#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();
}

The places where UB! is mentioned are undefined behavior, and the actual behavior is likely to depend on many factors (a.m might be equal to 42, 0, or some other garbage). The places where ~UB is mentioned are also undefined behavior in theory, but in practice, due the use of a placement new, it's very unlikely than a->m will be equal to anything else than 42.