12
votes

I have a piece of simple C++ code, in which I defined a template and a global object by specializing the template. The object constructor accesses a static member in the specialized template. But it turns out the static member is not initialized at that point. But for a local object (defined in the body of a function), it works. I'm confused...

My c++ compiler is: g++ (Ubuntu 5.4.0-6ubuntu1~16.04.4) 5.4.0 20160609

/////////////////////////
template<typename T>
class TB{
public:
  const char *_name;
  TB(const char * str):_name(str){
    cout << "constructor is called:" << _name << endl;
  };

  virtual ~TB(){
    cout << "destructor is called:" << _name << endl;
  };
};

template<typename T>
class TA{
public:
  const char *_name;
  TA(const char * str):_name(str){
    cout << "constructor is called:" << _name << endl;
    cout << tb._name <<endl;
  };

  virtual ~TA(){
    cout << "destructor is called:" << _name << endl;
  };

  static TB<T> tb;
};

template<typename T>
  TB<T> TA<T>::tb("static-tb");
TA<int> ta("global-ta");

int main(int argc,char ** argv){
  cout << "program started." << endl;
  cout << "program stopped." << endl;
  return 0;
}

/////////////////////////
//  OUTPUT:
constructor is called:global-ta
// yes, only such a single line.

If I put the definition of ta in main() like the following, it works.

int main(int argc,char ** argv){
  cout << "program started." << endl;
  TA<int> ta("local-ta");
  cout << "program stopped." << endl;
  return 0;
}

/////////////////////
//  OUTPUT:
constructor is called:static-tb
program started.
constructor is called:local-ta
static-tb
program stopped.
destructor is called:local-ta
destructor is called:static-tb
// end of output
1
Can you try a different compiler, such as Clang or GCC 7?John Zwinck
change the cout to printf see the output. TB<T> TA<T>::tb("static-tb"); is not instantiated, ref stackoverflow.com/questions/6959665/…Liu Hao
why even "program started" was not printed?suzuiyue
@JohnZwinck I could reproduce it with VS2015.A.S.H

1 Answers

9
votes

In the first scenario, your program crashed before main started. It crashes inside the constructor of ta because it accesses tb which was not yet constructed. This is a form of the Static Initialization Order Fiasco

The second scenario was successful because ta is inside main, and that guaranteed that tb which is non-local, was constructed before ta.

The question is, why in the first scenario, ta was constructed before tb even though tb and ta were defined in the same compilation unit, with tb defined before ta?

Knowing that tb is a template static data member, this quote from cppreference applies:

Dynamic initialization

After all static initialization is completed, dynamic initialization of non-local variables occurs in the following situations:

1) Unordered dynamic initialization, which applies only to (static/thread-local) variable templates and (since C++11) class template static data members that aren't explicitly specialized. Initialization of such static variables is indeterminately sequenced with respect to all other dynamic initialization except if the program starts a thread before a variable is initialized, in which case its initialization is unsequenced (since C++17). Initialization of such thread-local variables is unsequenced with respect to all other dynamic initialization.

So there's no sequence guaranteed here! Since ta is a static variable with an explicit template specialization, the compiler was allowed to initialize it before tb.

Another quote from the same page says:

Early dynamic initialization

The compilers are allowed to initialize dynamically-initialized variables as part of static initialization (essentially, at compile time), if the following conditions are both true:

1) the dynamic version of the initialization does not change the value of any other object of namespace scope prior to its initialization

2) the static version of the initialization produces the same value in the initialized variable as would be produced by the dynamic initialization if all variables not required to be initialized statically were initialized dynamically. Because of the rule above, if initialization of some object o1 refers to an namespace-scope object o2, which potentially requires dynamic initialization, but is defined later in the same translation unit, it is unspecified whether the value of o2 used will be the value of the fully initialized o2 (because the compiler promoted initialization of o2 to compile time) or will be the value of o2 merely zero-initialized.

The compiler has decided according to these rules to promote the initialization of ta before tb. Whether it was promoted to static initialization is not clear, but in any case, it seems pretty clear that the sequence of initialization is not guaranteed when it comes to variable templates and static templates members, due to the first quote and the promotion rules of the second quote.

Solution

To ensure that tb is initialized before it is used, the simplest is to put it inside a wrapper function. I think that this should be somehow a rule of thumb when dealing with static templates members:

template<typename T>
class TA{
    //...
    static TB<T>& getTB();
};

template<typename T>
TB<T>& TA<T>::getTB()
{ static TB<T> tb("static-tb");
  return tb;
}