Search code examples
c++c++11language-lawyerstatic-order-fiasco

Deferred initialisation order in C++11


Consider the following code, split across three compilation units:

a.h:

struct A
{
    void Register(const char* s);

    const char* m_s[10];
    int m_i = 0;
};

A& GetA();

a.cpp:

#include "a.h"
#include <stdio.h>

void A::Register(const char* s)
{
    m_s[m_i++] = s;
}

A& GetA()
{
    static A instance;
    return instance;
}

int main(int argc, char* argv[])
{
    A& a = GetA();
    int n = a.m_i;
    for (int i = 0; i < n ; ++i)
        printf("%s\n", a.m_s[i]);
    return 0;
}

b.cpp:

#include "a.h"
struct B
{
    B() { GetA().Register("b"); }

    static B* instance;
};
B* B::instance = new B;

c.cpp:

#include "a.h"
struct C
{
    C() { GetA().Register("c"); }

    static C* instance;
};
C* C::instance = new C;

The code builds and runs fine using gcc (-std=c++11), producing the output:

c
b

Now, referring to cppreference.com:

Deferred dynamic initialization

It is implementation-defined whether dynamic initialization happens-before the first statement of the main function (for statics) or the initial function of the thread (for thread-locals), or deferred to happen after.

If the initialization of a non-inline variable is deferred to happen after the first statement of main/thread function, it happens before the first odr-use of any variable with static/thread storage duration defined in the same translation unit as the variable to be initialized. If no variable or function is odr-used from a given translation unit, the non-local variables defined in that translation unit may never be initialized (this models the behavior of an on-demand dynamic library). However, as long as anything from a TU is odr-used, all non-local variables whose initialization or destruction has side effects will be initialized even if they are not used in the program.

Note that a.cpp is unaware of the existence of B and C, and that the only interactions of B & C with A are the invocations of GetA() and A::Register() during construction of their respective instances.

As far as I can see, the B & C instances are not ODR-used, and certainly not from main()'s translation unit. Their initialisation clearly has side effects, but it seems to me that there's no guarantee that this initialisation will occur before entry to main(), or before main() prints the registered strings - or indeed at all.

So - finally - my question is this: Is the fact that the B and C instances are initialised before main() prints the registered strings due not to the standard, but instead to gcc's implementation-defined behaviour?

If it is guaranteed by the standard, how?


Solution

  • Is the fact that the B and C instances are initialised before main() prints the registered strings due not to the standard, but instead to gcc's implementation-defined behaviour?

    It is not guaranteed by standard. The most relevant part of the quote:

    If no variable or function is odr-used from a given translation unit, the non-local variables defined in that translation unit may never be initialized

    Since no function, nor variable has been odr used from b.cpp nor c.cpp, their static variables may be uninitialized (in regard to dynamic initialization) and therefore the side-effects of their initialization might not be visible.


    In practice, I would expect the shown, initializing behaviour when the translation units are statically linked, and the possible non-initializing behaviour when they are dynamically loaded (shared library). But neither is guaranteed by the standard, as it doesn't specify how shared libraries behave.