Default, value and zero initialization mess
Asked Answered
R

3

96

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 C++14).

I am quoting and trying to extend a really good answer Value-/Default-/Zero- Init C++98 and C++03 here to make it more general as it would help a lot of users if somebody could help fill out the needed gaps to have a good overview about what happens when?

The full insight by examples in a nutshell:

Sometimes the memory returned by the new operator will be initialized, and sometimes it won't depending on whether the type you're newing up is a POD (plain old data), or if it's a class that contains POD members and is using a compiler-generated default constructor.

  • In C++1998 there are 2 types of initialization: zero- and default-initialization
  • In C++2003 a 3rd type of initialization, value-initialization was added.
  • In C++2011/C++2014 only list-initialization was added and the rules for value-/default-/zero-initialization changed a bit.

Assume:

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 */

In a C++98 compiler, the following should occur:

  • new A - indeterminate value (A is POD)
  • new A()- zero-initialize
  • new B - default construct (B::m is uninitialized, B is non-POD)
  • new B() - default construct (B::m is uninitialized)
  • new C - default construct (C::m is zero-initialized, C is non-POD)
  • new C() - default construct (C::m is zero-initialized)
  • new D - default construct (D::m is uninitialized, D is non-POD)
  • new D() - default construct? (D::m is uninitialized)

In a C++03 conformant compiler, things should work like so:

  • new A - indeterminate value (A is POD)
  • new A() - value-initialize A, which is zero-initialization since it's a POD.
  • new B - default-initializes (leaves B::m uninitialized, B is non-POD)
  • new B() - value-initializes B which zero-initializes all fields since its default ctor is compiler generated as opposed to user-defined.
  • new C - default-initializes C, which calls the default ctor. (C::m is zero-initialized, C is non-POD)
  • new C() - value-initializes C, which calls the default ctor. (C::m is zero-initialized)
  • new D - default construct (D::m is uninitialized, D is non-POD)
  • new D() - value-initializes D?, which calls the default ctor (D::m is uninitialized)

Italic values and ? are uncertainties, please help to correct this :-)

In a C++11 conformant compiler, things should work like so:

??? (please help if I start here it will anyway go wrong)

In a C++14 conformant compiler, things should work like so: ??? (please help if I start here it will anyway go wrong) (Draft based on answer)

  • new A - default-initializes A, compiler gen. ctor, (leavs A::m uninitialized) (A is POD)

  • new A() - value-initializes A, which is zero-initialization since 2. point in [dcl.init]/8

  • new B - default-initializes B, compiler gen. ctor, (leavs B::m uninitialized) (B is non-POD)

  • new B() - value-initializes B which zero-initializes all fields since its default ctor is compiler generated as opposed to user-defined.

  • new C - default-initializes C, which calls the default ctor. (C::m is zero-initialized, C is non-POD)

  • new C() - value-initializes C, which calls the default ctor. (C::m is zero-initialized)

  • new D - default-initializes D (D::m is uninitialized, D is non-POD)

  • new D() - value-initializes D, which calls the default ctor (D::m is uninitialized)

  • new E - default-initializes E, which calls the comp. gen. ctor. (E::m is uninitialized, E is non-POD)

  • new E() - value-initializes E, which zero-initializes E since 2 point in [dcl.init]/8 )

  • new F - default-initializes F, which calls the comp. gen. ctor. (F::m is uninitialized, F is non-POD)

  • new F() - value-initializes F, which default-initializes F since 1. point in [dcl.init]/8 (F ctor function is user-provided if it is user-declared and not explicitly defaulted or deleted on its first declaration. Link)

Roeder answered 21/4, 2015 at 7:44 Comment(4)
there's a good explanation of it here: en.cppreference.com/w/cpp/language/default_constructorWineshop
As far as I can tell, there's only a difference between C++98 and C++03 in these examples. The issue seems to be described in N1161 (there are later revisions of that document) and CWG DR #178. The wording needed to change in C++11 due to new features and a new specification of POD, and it changed again in C++14 due to defects in the C++11 wording, but the effects in these cases are not changed.Formality
While boring, struct D { D() {}; int m; }; may be worth including in your list.Cutcheon
There is a nice an disturbing poster which brings this mess to the point: randomcat.org/cpp_initialization/initialization.pngRoeder
H
26

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.
Hesler answered 21/4, 2015 at 7:56 Comment(4)
ah so you are talking mainly about c++14 and references for c++11 are given in bracketsRoeder
@Roeder Correct. I mean, C++14 is the latest standard, so that is to the fore.Hesler
The annoying thing about trying to trace initialization rules across standards is that a lot of the changes (most? all?) between the published C++14 and C++11 standards happened via DRs, and so are de facto C++11. And then there are also post-C++14 DRs...Ambulator
@Hesler I still don't understand why struct A { int m; }; struct C { C() : m(){}; int m; };produce different results and what causes m in A to be initialized in the first place. I have opened a dedicated thread for the experiment that i did and i will appreciate your input there to clarify the issue. Thanks stackoverflow.com/questions/45290121/…Grijalva
D
12

The following answer extends the answer https://mcmap.net/q/17068/-do-the-parentheses-after-the-type-name-make-a-difference-with-new 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 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 }
    { }

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
Dall answered 21/4, 2015 at 8:50 Comment(0)
Z
1

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.

Zing answered 19/2, 2020 at 19:38 Comment(4)
Some tests seem to fail for me even using GCC7.4: godbolt.org/z/PocEGz6sq... could be that the GTest version matters? This is weird, because the code seems to be part of your test suite on GitHub, where the test still passes (or at least did 5 months ago?)Dexter
@AdomasBaliuka Thanks for the heads up. I'll be back to coding on this soon so I'll see if the tests still pass. I remember some tests didn't pass on some platforms and I had to make changes, not sure if it was before or after this SO post. Note that I'm now using C++17, so results might differ too because of this.Zing
@AdomasBaliuka I added a GitHub issue to track this, if you're interested: github.com/vgc/vgc/issues/566Zing
@AdomasBaliuka I just had some time to look at this. You're right, the tests marked "UB!" do fail in some machines/compilers. And in fact, I had already commented them out in my production codebase back in 2020-12-14 (github.com/vgc/vgc/commit/1d3f492e6), reason why my tests pass. The morale of the story is: those are definitely undefined behavior, so, well, the behavior is undefined. The value is not supposed to be zero-initialized, but it may happen to be zero anyway by "chance" or other factors.Zing

© 2022 - 2024 — McMap. All rights reserved.