C++, where initializing variables is a topic of debate among experts

Written by

in

Let me say first: I did not come up with the title myself; credit goes to the cpplang Slack. Unfortunately, I forgot who actually said it.

So what is so interesting about initializing a variable, you might ask? Let’s take a simple struct:

struct Foo
{
    int x;
    int y;
};

And create an object (in a local scope):

void f()
{
     Foo foo; 
}

This object will have two uninitialized members, x and y, because the standard says: “only initialize local variables when explicitly requested”. Actually, that is a lie; the standard says no such thing. But it is basically what it comes down to for local variables.

Global and static variables are always zero-initialized before the program starts. However, I’m having difficulty interpreting what the standard says about local variables, so let’s do some testing.

Full code

#include <string>
#include <iostream>

struct FooBar
{
    FooBar()
    {
        std::cout << "FooBar init, ";
    }
};

struct Bar
{
    int x;
    int y;
    FooBar fb;
};

struct BarKeep
{
    BarKeep() : bar(Bar()) {}
    Bar bar;
};

void dump(std::string msg, Bar& b)
{
    std::cout << msg << "{ " << b.x << ", " << b.y << " }\n";
}

int main()
{
    Bar a = { 1, 2 };
    dump("a = ", a);    // OK

    Bar b;
    dump("b = ", b);    // Uninitialized!

    auto e = Bar();     // VS2013: Copy of Uninitialized == still Uninitialized
    dump("e = ", e);

    Bar f = Bar();      // VS2013: Copy of Uninitialized == still Uninitialized
    dump("f = ", f);

    BarKeep h{};        // VS2013: Copy of Uninitialized == still Uninitialized
    dump("h = ", h.bar);

    Bar c{};            // OK
    dump("c = ", c);

    Bar d = {};         // OK
    dump("d = ", d);

    auto g = Bar{};     // OK
    dump("g = ", g);
}

Output of VS2013:

FooBar init, a = { 1, 2 }
FooBar init, b = { 16853760, 4 }
FooBar init, e = { 16853760, 4 }
FooBar init, f = { 16853760, 4 }
FooBar init, h = { 16853760, 4 }
FooBar init, c = { 0, 0 }
FooBar init, d = { 0, 0 }
FooBar init, g = { 0, 0 }

Output of Clang 7.0 / GCC 7/8:

FooBar init, a = { 1, 2 }   // compiler warning: missing field 'fb' initializer
FooBar init, b = { 0, 4198208 }
FooBar init, e = { 0, 0 }
FooBar init, f = { 0, 0 }
FooBar init, h = { 0, 0 }
FooBar init, c = { 0, 0 }
FooBar init, d = { 0, 0 }
FooBar init, g = { 0, 0 }

The first thing we notice is that the constructor of FooBar is called, yet the members of Foo can still be uninitialized. This is because we have to distinguish between ‘default-initialized’ and ‘value-initialized’.

For a, Bar is explicitly value-initialized. That means constructors are called and basic types are assigned their default value (0). However, b is ‘default-initialized’, which means that constructors are called, but no initialization is performed for basic types. Not only is the constructor of the object called, but also the constructors of any nested types.

Although it looks like the compilers disagree about the initialization, uninitialized values on the stack can be anything, and reading from an uninitialized value causes undefined behaviour (UB). So we can’t really draw any conclusions at this point.

I have tried filling the stack with garbage between tests, but the results were unaffected.

Since compilers seem to disagree about:

auto b = Bar(); 
Bar b = Bar();    

I think it should be avoided, and:

Bar b = {};

is going to be (and has been) my preferred syntax, because it seems to value-initialize correctly on all the compilers I tested here.

Conclusions

  • The brace syntax is a good way to make sure you are value-initializing objects that have no constructor.
  • If you make sure constructors always initialize all member variables of basic types, you will never have uninitialized values in those objects, no matter what syntax is used.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *