C++ zero initialization - Why is `b` in this program uninitialized, but `a` is initialized?

The issue here is pretty subtle. You would think that

bar::bar() = default;

would give you a compiler generated default constructor, and it does, but it is now considered user provided. [dcl.fct.def.default]/5 states:

Explicitly-defaulted functions and implicitly-declared functions are collectively called defaulted functions, and the implementation shall provide implicit definitions for them ([class.ctor] [class.dtor], [class.copy.ctor], [class.copy.assign]), which might mean defining them as deleted. A function is user-provided if it is user-declared and not explicitly defaulted or deleted on its first declaration. A user-provided explicitly-defaulted function (i.e., explicitly defaulted after its first declaration) is defined at the point where it is explicitly defaulted; if such a function is implicitly defined as deleted, the program is ill-formed. [ Note: Declaring a function as defaulted after its first declaration can provide efficient execution and concise definition while enabling a stable binary interface to an evolving code base. — end note ]

emphasis mine

So we can see that since you did not default bar() when you first declared it, it is now considered user provided. Because of that [dcl.init]/8.2

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;

no longer applies and we are not value initializing b but instead default initializing it per [dcl.init]/8.1

if T is a (possibly cv-qualified) class type ([class]) with either no default constructor ([class.default.ctor]) or a default constructor that is user-provided or deleted, then the object is default-initialized;


The difference in behaviour comes from the fact that, according to [dcl.fct.def.default]/5, bar::bar is user-provided where foo::foo is not1. As a consequence, foo::foo will value-initialize its members (meaning: zero-initialize foo::a) but bar::bar will stay uninitialized2.


1) [dcl.fct.def.default]/5

A function is user-provided if it is user-declared and not explicitly defaulted or deleted on its first declaration.

2)

From [dcl.init#6]:

To value-initialize an object of type T means:

  • if T is a (possibly cv-qualified) class type with either no default constructor ([class.ctor]) 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;

  • ...

From [dcl.init.list]:

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

  • ...

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

From Vittorio Romeo's answer


From cppreference:

Aggregate initialization initializes aggregates. It is a form of list-initialization.

An aggregate is one of the following types:

[snip]

  • class type [snip], that has

    • [snip] (there are variations for different standard versions)

    • no user-provided, inherited, or explicit constructors (explicitly defaulted or deleted constructors are allowed)

    • [snip] (there are more rules, which apply to both classes)

Given this definition, foo is an aggregate, while bar is not (it has user-provided, non-defaulted constructor).

Therefore for foo, T object {arg1, arg2, ...}; is syntax for aggregate initialisation.

The effects of aggregate initialization are:

  • [snip] (some details irrelevant to this case)

  • If the number of initializer clauses is less than the number of members or initializer list is completely empty, the remaining members are value-initialized.

Therefore a.a is value initialised, which for int means zero initialisation.

For bar, T object {}; on the other hand is value initialisation (of the class instance, not value initialisation of members!). Since it is a class type with a default constructor, the default constructor is called. The default constructor that you defined default initialises the members (by virtue of not having member initialisers), which in case of int (with non-static storage) leaves b.b with an indeterminate value.

And for pod-types, the default initialization is zero-initialization.

No. This is wrong.


P.S. A word about your experiment and your conclusion: Seeing that output is zero does not necessarily mean that the variable was zero initialised. Zero is perfectly possible number for a garbage value.

for that I ran the program maybe 5~6 times before posting and about 10 times now, a is always zero. b changes around a little.

The fact that the value was same multiple times does not necessarily mean that it was initialised either.

I also tried with set(CMAKE_CXX_STANDARD 14). The result was the same.

The fact that result is the same with multiple compiler options doesn't mean that the variable is initialised. (Although in some cases, changing standard version can change whether it is initialised).

How could I somehow shake my RAM a little so that if there was zero there, it should now be something else

There is no guaranteed way in C++ to make uninitialised value value to appear nonzero.

Only way to know that a variable is initialised is to compare program to the rules of the language and verify that the rules say that it is initialised. In this case a.a is indeed initialised.