[ Team LiB ] Previous Section Next Section

Gotcha #55: Runtime Static Initialization Order

All static data in a C++ program are initialized before access. Most of these static initializations are accomplished when the program image is loaded, before execution begins. If no explicit initializer is provided, the data are initialized to "all zeros":



static int question; // 0 


extern int answer = 42;


const char *terminalType; // null


bool isVT100; // false


const char **ptt = &terminalType;


These initializations all take place "simultaneously," with no issue of initializer ordering.

We can also employ runtime static initialization. In this case, there is no guarantee of initialization order between translation units. (A translation unit is basically a preprocessed file.) This is a frequent source of bugs, since initialization order may change without source code change:



// in file term.cpp 


const char *terminalType = getenv( "TERM" );





// in file vt100.cpp


extern const char *terminalType;


bool isVT100 = strcmp( terminalType, "vt100" )==0; // error?


There is an implicit ordering dependency between the initializations of terminalType and isVT100, but the C++ language does not, and cannot, guarantee a particular initialization order. This gotcha typically occurs when an existing, working program is ported to a different platform that happens to implement a different translation unit ordering for runtime static initializations. It may also pop up without source changes due to changes in a build procedure or if a facility that was formerly statically linked is changed to use dynamic linking.

Keep in mind that default initialization of static class objects also constitutes a runtime static initialization:



class TermInfo { 


 public:


   TermInfo()


       : type_( ::terminalType )


       {}


 private:


   std::string type_;


};


// . . .


TermInfo myTerm; // runtime static init!


The best way to avoid runtime static initialization difficulties is to minimize the use of external variables, including static class data members (see Gotcha #3).

Failing that, another possibility is to depend only on the initialization order within a given translation unit. This ordering is well defined, and the static variables within a translation unit are initialized in the order in which they are defined. For example, if the definitions for terminalType and isVT100 occurred in that order within the same file, there would be no portability issue. Even with this procedure, however, an initialization order problem may occur if an external function, including member functions, uses a static variable, since that function may be called, directly or indirectly, from runtime static initializations of other translation units:



extern const char *termType() 


   { return terminalType; }


Failing that, another approach might be to substitute lazy evaluation for initialization. Typically, this is accomplished with some variation of the Singleton pattern (see Gotcha #3).

As a last resort, we can code the initialization order explicitly, using standard techniques. One such standard technique is a Schwarz counter, so called because it was devised by Jerry Schwarz and is employed in his implementation of the iostream library:

gotcha55/term.h



extern const char *terminalType; 


//other things to initialize . . .


class InitMgr { // Schwarz counter


 public:


   InitMgr()


       { if( !count_++ ) init(); }


   ~InitMgr()


       { if( !--count_ ) cleanup(); }


   void init();


   void cleanup();


 private:


   static long count_; // one per process


};


namespace { InitMgr initMgr; } // one per file inclusion


gotcha55/term.cpp



extern const char *terminalType = 0; 


long InitMgr::count_ = 0;


void InitMgr::init() {


   if( !(terminalType = getenv( "TERM" )) )


       terminalType = "VT100";


   // other initializations . . .


}


void InitMgr::cleanup() {


   // any required cleanup . . .


}


A Schwarz counter counts how many times the header file in which it resides is #included. There is a single instance, per process, of the static member count_ of InitMgr. However, every time the header file term.h is included, a new object of type InitMgr is allocated, and each of these requires a runtime static initialization. The InitMgr constructor checks the count_ member to see if this is the "first" initialization of an InitMgr object of the process. If it is, the initializations are performed.

Conversely, when the process terminates normally, static objects that have destructors will be destroyed. With each InitMgr object destruction, the InitMgr destructor decrements the count_. When count_ reaches zero, any required cleanup is performed.

Although they are robust, particularly boneheaded coding can defeat even Schwarz counters. In general, it's best to minimize use of static variables and avoid runtime static initializations.

    [ Team LiB ] Previous Section Next Section