I am very confused about value- & default- & zero-initialization. and especially when they kick in for the different standards C++03 and C++11 (and
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).
//
#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.
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
Tinitializes 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
Tmeans:
- if
Tis a (possibly cv-qualified) class type (Clause 9), the default constructor (12.1) forTis called (and the initialization is ill-formed ifThas 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
Tis 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
Tmeans:
- if
Tis 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
Tis 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
Tis 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
Tmeans:
- if
Tis a (possibly cv-qualified) class type (Clause 9) with a user-provided constructor (12.1), then the default constructor forTis called (and the initialization is ill-formed if T has no accessible default constructor);- if
Tis a (possibly cv-qualified) non-union class type without a user-provided constructor, then the object is zero-initialized and, ifT’s implicitly-declared default constructor is non-trivial, that constructor is called.- if
Tis 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
C++11 (In reference to n3242)
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 assignment-expression 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 }
    { }
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.
Performed in the following cases
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.
Performed in the following cases
To default-initialize an object of type T means:
- if T is a (possibly cv-qualified)
non-PODclass 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.
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