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

C++InitializationLanguage Lawyer

C++ Problem Overview


According to the accepted (and only) answer for this Stack Overflow question,

> Defining the constructor with > > MyTest() = default; > > will instead zero-initialize the object.

Then why does the following,

#include <iostream>

struct foo {
    foo() = default;
    int a;
};

struct bar {
    bar();
    int b;
};

bar::bar() = default;

int main() {
    foo a{};
    bar b{};
    std::cout << a.a << ' ' << b.b;
}

produce this output:

0 32766

Both constructors defined are default? Right? And for POD types, the default initialization is zero-initialization.

And according to the accepted answer for this question,

> 2. If a POD member is not initialized in the constructor nor via C++11 > in-class initialization, it is default-initialized. > > 3. The answer is the same regardless of stack or heap. > > 4. In C++98 (and not afterward), new int() was specified as performing > zero initialization.

Despite trying to wrap my (albeit tiny) head around default constructors and default initialization, I couldn't come up with an explanation.

C++ Solutions


Solution 1 - C++

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;

Solution 2 - C++

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

Solution 3 - C++

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.

Solution 4 - C++

Meh, I tried running the snippet you provided as test.cpp, through gcc & clang and multiple optimization levels:

steve@steve-pc /tmp> g++ -o test.gcc.O0 test.cpp
                                                                              [ 0s828 | Jan 27 01:16PM ]
steve@steve-pc /tmp> g++ -o test.gcc.O2 -O2 test.cpp
                                                                              [ 0s901 | Jan 27 01:16PM ]
steve@steve-pc /tmp> g++ -o test.gcc.Os -Os test.cpp
                                                                              [ 0s875 | Jan 27 01:16PM ]
steve@steve-pc /tmp> ./test.gcc.O0
0 32764                                                                       [ 0s004 | Jan 27 01:16PM ]
steve@steve-pc /tmp> ./test.gcc.O2
0 0                                                                           [ 0s004 | Jan 27 01:16PM ]
steve@steve-pc /tmp> ./test.gcc.Os
0 0                                                                           [ 0s003 | Jan 27 01:16PM ]
steve@steve-pc /tmp> clang++ -o test.clang.O0 test.cpp
                                                                              [ 1s089 | Jan 27 01:17PM ]
steve@steve-pc /tmp> clang++ -o test.clang.Os -Os test.cpp
                                                                              [ 1s058 | Jan 27 01:17PM ]
steve@steve-pc /tmp> clang++ -o test.clang.O2 -O2 test.cpp
                                                                              [ 1s109 | Jan 27 01:17PM ]
steve@steve-pc /tmp> ./test.clang.O0
0 274247888                                                                   [ 0s004 | Jan 27 01:17PM ]
steve@steve-pc /tmp> ./test.clang.Os
0 0                                                                           [ 0s004 | Jan 27 01:17PM ]
steve@steve-pc /tmp> ./test.clang.O2
0 0                                                                           [ 0s004 | Jan 27 01:17PM ]
steve@steve-pc /tmp> ./test.clang.O0
0 2127532240                                                                  [ 0s002 | Jan 27 01:18PM ]
steve@steve-pc /tmp> ./test.clang.O0
0 344211664                                                                   [ 0s004 | Jan 27 01:18PM ]
steve@steve-pc /tmp> ./test.clang.O0
0 1694408912                                                                  [ 0s004 | Jan 27 01:18PM ]

So that is where it gets interesting, it clearly shows clang O0 build is reading random numbers, presumably stack space.

I quickly turned up my IDA to see what's happening:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  __int64 v3; // rax
  __int64 v4; // rax
  int result; // eax
  unsigned int v6; // [rsp+8h] [rbp-18h]
  unsigned int v7; // [rsp+10h] [rbp-10h]
  unsigned __int64 v8; // [rsp+18h] [rbp-8h]

  v8 = __readfsqword(0x28u); // alloca of 0x28
  v7 = 0; // this is foo a{}
  bar::bar((bar *)&v6); // this is bar b{}
  v3 = std::ostream::operator<<(&std::cout, v7); // this is clearly 0
  v4 = std::operator<<<std::char_traits<char>>(v3, 32LL); // 32 = 0x20 = ' '
  result = std::ostream::operator<<(v4, v6); // joined as cout << a.a << ' ' << b.b, so this is reading random values!!
  if ( __readfsqword(0x28u) == v8 ) // stack align check
    result = 0;
  return result;
}

Now, what does bar::bar(bar *this) does?

void __fastcall bar::bar(bar *this)
{
  ;
}

Hmm, nothing. We had to resort to using assembly:

.text:00000000000011D0                               ; __int64 __fastcall bar::bar(bar *__hidden this)
.text:00000000000011D0                                               public _ZN3barC2Ev
.text:00000000000011D0                               _ZN3barC2Ev     proc near               ; CODE XREF: main+20↓p
.text:00000000000011D0
.text:00000000000011D0                               var_8           = qword ptr -8
.text:00000000000011D0
.text:00000000000011D0                               ; __unwind {
.text:00000000000011D0 55                                            push    rbp
.text:00000000000011D1 48 89 E5                                      mov     rbp, rsp
.text:00000000000011D4 48 89 7D F8                                   mov     [rbp+var_8], rdi
.text:00000000000011D8 5D                                            pop     rbp
.text:00000000000011D9 C3                                            retn
.text:00000000000011D9                               ; } // starts at 11D0
.text:00000000000011D9                               _ZN3barC2Ev     endp

So yeah, it's just, nothing, what the constructor basically does is this = this. But we know that it is actually loading random uninitialized stack addresses and print it.

What if we explicitly provide values for the two structs?

#include <iostream>

struct foo {
    foo() = default;
    int a;
};

struct bar {
    bar();
    int b;
};

bar::bar() = default;

int main() {
    foo a{0};
    bar b{0};
    std::cout << a.a << ' ' << b.b;
}

Hit up clang, oopsie:

steve@steve-pc /tmp> clang++ -o test.clang.O0 test.cpp
test.cpp:17:9: error: no matching constructor for initialization of 'bar'
    bar b{0};
        ^~~~
test.cpp:8:8: note: candidate constructor (the implicit copy constructor) not viable: no known conversion
      from 'int' to 'const bar' for 1st argument
struct bar {
       ^
test.cpp:8:8: note: candidate constructor (the implicit move constructor) not viable: no known conversion
      from 'int' to 'bar' for 1st argument
struct bar {
       ^
test.cpp:13:6: note: candidate constructor not viable: requires 0 arguments, but 1 was provided
bar::bar() = default;
     ^
1 error generated.
                                                                              [ 0s930 | Jan 27 01:35PM ]

Similar fate with g++ as well:

steve@steve-pc /tmp> g++ test.cpp
test.cpp: In function ‘int main()’:
test.cpp:17:12: error: no matching function for call to ‘bar::bar(<brace-enclosed initializer list>)’
     bar b{0};
            ^
test.cpp:8:8: note: candidate: ‘bar::bar()’
 struct bar {
        ^~~
test.cpp:8:8: note:   candidate expects 0 arguments, 1 provided
test.cpp:8:8: note: candidate: ‘constexpr bar::bar(const bar&)’
test.cpp:8:8: note:   no known conversion for argument 1 from ‘int’ toconst bar&’
test.cpp:8:8: note: candidate: ‘constexpr bar::bar(bar&&)’
test.cpp:8:8: note:   no known conversion for argument 1 from ‘int’ to ‘bar&&’
                                                                              [ 0s718 | Jan 27 01:35PM ]

So this means it's effectively a direct initialization bar b(0), not aggregate initialization.

This is probably because if you do not provide an explicit constructor implementation this could potentially be an external symbol, for example:

bar::bar() {
  this.b = 1337; // whoa
}

The compiler isn't smart enough to deduce this as a no-op/an inline call in a non-optimized stage.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionDuck DodgersView Question on Stackoverflow
Solution 1 - C++NathanOliverView Answer on Stackoverflow
Solution 2 - C++YSCView Answer on Stackoverflow
Solution 3 - C++eerorikaView Answer on Stackoverflow
Solution 4 - C++Steve FanView Answer on Stackoverflow