Default, Value and Zero Initialization Mess

Default, value and zero initialization mess

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.

Value initialization: default initialization or zero initialization?

So, if I read correctly all of that, in the function foo above,
bar.value should always be initialized with 0 and it should never be
initialized with garbage, am I right?

Yes. Your object is direct-list-initialized. C++14's* [dcl.init.list]/3 specifies that

List-initialization of an object or reference of type T is defined
as follows:

  • [… Inapplicable bullet points…]

  • Otherwise, if T is an aggregate, aggregate initialization is performed (8.5.1).

  • Otherwise, if the initializer list has no elements and T is a class type with a default constructor, the object is value-initialized.

  • […]

Your class isn't an aggregate since it has user-provided constructors, but it does have a default constructor. [dcl.init]/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;

[dcl.fct.def.default]/4:

A special member function is user-provided if it is user-declared and
not explicitly defaulted […] on its first declaration.

So your constructor is not user-provided, therefore the object is zero-initialized. (The constructor is not called since its trivial)

And finally, in case this was not clear, to zero-initialize an object or reference of type T means:

  • if T is a scalar type (3.9), the object is initialized to the value obtained by converting the integer literal 0 (zero) 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;

  • […]


Thus either

  • Your compiler is bugged

  • …or your code triggers undefined behavior at some other point.


* The answer is still yes in C++11, though the quoted sections are not equivalent.

Why is value-initialization specified as not calling trivial default constructors?

I haven't been able to track down an authoritative answer to my question. However, looking at the C++03 standard and the CWG issues list, I have a rough idea.

Let's review value-initialization in C++03. It is defined for a type T as follows:

  • If T is a class type with a user-declared constructor, then the default constructor is called;
  • If T is a non-union class type without a user-declared constructor [which implies that the compiler implicitly declares a default constructor], then each base class subobject and member subobject is value-initialized;
  • [...]

The second bullet point ensures that direct subobjects of scalar type are zero-initialized and is thus "stronger" than simply calling the implicit default constructor. This was the intent behind value-initialization.

So the wording in C++03 makes sense and (without needing to make any special exception) leads to the following result: when the default constructor is trivial, no function calls actually occur.

In C++11, the definition of zero-initialization was changed so that it would also initialize padding bits to zero (CWG 694). My guess is that the wording around value-initialization was changed in order to guarantee that value-initialization would also be guaranteed to zero out padding bits in the case of a trivial default constructor. So in C++11, when T (a type without a user-provided default constructor) is value-initialized, what happens is stronger than just value-initializing all direct subobjects of T. Rather, the entire T object is zero-initialized first (to ensure padding is zeroed out) and then the default constructor is called.

But again, our question is why C++11 carves out a special exception in the case where the default constructor is trivial, preventing it from being called. The paper, N2762, that made the wording change did not explain why that exception was put in, but we can now see that it preserves the C++03 behaviour wherein a trivial default constructor was not called at all. My guess is that the authors put in the wording intentionally to preserve this behaviour, but their motivations are unclear.

One possible motivation is that trivial default constructors were, in general, not constexpr in C++11, except in the case of empty classes: they would leave scalar members uninitialized, which was not allowed. Therefore, omitting the call to the default constructor during value-initialization makes it possible to possible to value-initialize objects that are trivially default constructible, which is desirable to allow; see CWG 644. However, there is no indication of whether the authors of N2762 intended to allow this. (Aside: for subsequent developments related to CWG 644, see CWG 1452.) Note that in C++20, such trivial default constructors became constexpr. See relevant SO question.

I find it more likely that the authors of N2762 were simply being cautious: in other words, they probably wanted to preserve the C++03 behaviour of not calling the trivial default constructor just in case changing this would cause problems they didn't anticipate. (It probably doesn't have anything to do with performance, though; the compiler can optimize out a call to a trivial default constructor.)

Still, we should observe that the C++11 behaviour isn't quite the same as the C++03 behaviour plus zero-initialization of padding. Let's say we have a type like this:

struct T {
struct U { U() {} } u;
int x;
};

In C++03, value-initialization of T means that U::U is called and then x is zero-initialized. In C++11, it means that the entire T object is zero-initialized and then T::T is called, which in turn calls U::U. So C++11 has an extra function call here compared to C++03. So despite the authors' best efforts, the behaviour is not the same. As far as I can tell though, this difference doesn't break any code.

Does C++ default-initialization preserve prior zero-initialization?

Defect report 1787 lead to the change documented in N3914 being applied to the draft standard for C++14. Which change [dcl.init] paragraph 12 from:

If no initializer is specified for an object, the object is
default-initialized; if no initialization is performed, an object with
automatic or dynamic storage duration has indeterminate value. [ Note:
Objects with static or thread storage duration are zero-initialized,
see 3.6.2. — end note ]

to:

If no initializer is specified for an object, the object is
default-initialized. When storage for an object with automatic or
dynamic storage duration is obtained, the object has an indeterminate
value, and if no initialization is performed for the object, that
object retains an indeterminate value until that value is replaced

(5.17 [expr.ass]). [Note: Objects with static or thread storage
duration are zero-initialized, see 3.6.2 [basic.start.init]. —end
note] If an indeterminate value is produced by an evaluation, the
behavior is undefined except in the following cases:

[...]

This make it clear the indeterminate value situation only occurs for objects of automatic or dynamic storage duration. Since this was applied via a defect report it probably also applies to C++11 since the defect report occured before C++14 was accepted but it could apply further back as well. The rules for how far back a defect are supposed to apply were never clear to me.

Since placement new was brought up in the comments, the same change also modified section [expr.new], making the indeterminate value portion a comment:

If the new-initializer is omitted, the object is default-initialized
(8.5 [dcl.init]); if. [Note: If no initialization is performed, the
object has an indeterminate value. —end note]

The beginning of the section says:

[...]Entities created by a new-expression have dynamic storage
duration (3.7.4).[...]

Which seem sufficient to apply the changes in section [dcl.init].

This change was also interesting since prior to this change the term indeterminate value was not defined in the C++ standard.

Why is a FILE* pointer zero-initialized in C++'s default initialization?

You didn't initialize the pointers. They get random (undefined) values (specifically, whatever the runtime happened to leave on the stack during initialization before main was called). Sometimes those values happen to be NULL. C++ didn't initialize them, you just got "lucky". As noted in the comments on your question, on different compilers, or with different variable declaration orders, the FILE* often ends up non-NULL, because it's not being initialized, just inheriting whatever happened to already be on the stack.

C++ default initialization and value initialization: which is which, which is called when and how to reliably initialize a template-type member

Not so hard:

A x;
A * p = new A;

These two are default initialization. Since you don't have a user-defined constructor, this just means that all members are default-initialized. Default-initializing a fundamental type like int means "no initialization".

Next:

A * p = new A();

This is value initialization. (I don't think there exists an automatic version of this in C++98/03, though in C++11 you can say A x{};, and this brace-initialization becomes value-initialization. Moreover, A x = A(); is close enough practically despite being copy-initialization, or A x((A())) despite being direct-initialization.)

Again, in your case this just means that all members are value-initialized. Value initialization for fundamental types means zero-initialization, which in turn means that the variables are initialized to zero (which all fundamental types have).

For objects of class type, both default- and value-initialization invoke the default constructor. What happens then depends on the constructor's initializer list, and the game continues recursively for member variables.



Related Topics



Leave a reply



Submit