Search code examples
c++templatesinheritanceinline

Why is an inline static variable of a template class not initialized?


Minimal example and question

In this example, an inline static member variable c2 of a nontemplate class is initialized when the class member is instantiated, and member variable c1 of a template class is not. What is the difference? Why is c1 not being initialized unless I force it to be by taking its address, and c2 is initialized unconditionally?

struct C1 {                        
    C1() { std::cerr << "C1()\n"; }
};                                 


struct C2 {                        
    C2() { std::cerr << "C2()\n"; }
};                                 

template<typename T>               
struct Template {                  
    inline static C1 c1;           
};                                 

struct Nontemplate {                
    inline static C2 c2;           
};                                 

int main() {                       
    Template<int> a;               
    Nontemplate b;                  
    (void)a;                       
    (void)b;                       
}
// Output:
C2()                                  

Context and reasoning

Here's a bit of context to the minimal example. I have the Nontemplate class inherited from Template<something>, and the constructor of c2 depends on c1. I expect c1 to be created before c2; however, that is not the case.

template<typename T>                                                     
struct Template {                                                        
    inline static C1 c1;                                                 
};                                                                       

struct Nontemplate : public Template<int> {                               
    struct C2 {                                                          
        C2() {                                                           
            std::cerr << "Do something with Nontemplate::C1\n";          
            std::cerr << "&Nontemplate::c1 = " << &Nontemplate::c1 << "\n";
        }                                                                
    };                                                                   
    inline static C2 c2;                                                 
};                                                                       

int main() {                                                             
    Nontemplate b;                                                        
    (void)b;                                                             
}                                                                        
// Output:
Do something with Nontemplate::C1
&Nontemplate::c1 = 0x600ea8       
C1()                             

The code was compiled with g++ 7.2 with -std=c++17 flags. Both -O0 and -O2 give the same result.


Solution

  • Implicit instantiation of a class template only causes instantiation of the declarations it contains. Definitions are generally only instantiated when they are used in a context that requires the definition to exist.

    So as long as you don't use Template<int>::c1 in a way that would require its definition to exist (i.e. by odr-using it), then it will not be defined at all.

    One way of odr-using the variable is to take its address, as you mentioned.

    Even if you force the instantiation of the variable, there is no guarantee when exactly it will be initialized.

    C1's constructor is not constexpr and so the initialization of Nontemlate::c1 cannot be a constant expression. This means that you are going to get dynamic initialization of Template<int>::c1. Dynamic initialization of global static variables which are part of a template specialization are unordered, meaning that there is no guarantee in what order they will happen relative to any other dynamic initialization of global static variables.

    Similarly Nontemlate::c2 is not initialized by a constant expression and so is also dynamically initialized. Although Nontemlate::c2 has partially ordered dynamic initialization (being an inline variable not part of a template specialization), it is still indeterminately sequenced relative to Template<int>::c1 as explained above.

    It is also not strictly required that Template<int>::c1 and Nontemlate::c2 are initialized before main is entered. It is implementation-defined whether initialization is deferred until later, but before the first odr-use of the respective variable. I think this deferral is used mainly for runtime dynamic library loading, though.

    A common method to avoid ordering issues of global static storage duration variables is to use a function returning a reference to a local static variable instead, i.e.:

    template<typename T>               
    struct Template {                  
        static auto& c1() {
            static C1 instance; 
            return instance;
        }          
    };
    

    although this may have a performance impact when called often, because the construction of the local static must be checked each time.

    Alternative, if the initializer can be made a constant expression, making the variable constexpr should guarantee constant initialization, meaning that no dynamic initialization will happen at all and no ordering issues will be present.