Static
initialization dependency
Within
a specific translation unit, the order of initialization of static objects is
guaranteed to be the order in which the object definitions appear in that
translation unit.
The order of destruction is guaranteed to be the reverse of the order of
initialization.
However,
there is no guarantee concerning the order of initialization of static objects
across
translation units, and there’s no way to specify this order. This can
cause significant problems. As an example of an instant disaster (which will
halt primitive operating systems, and kill the process on sophisticated ones),
if one file contains
// First file
#include <fstream>
ofstream out("out.txt");
and
another file uses the
out
object in one of its initializers
// Second file
#include <fstream>
extern ofstream out;
class Oof {
public:
Oof() { out << "ouch"; }
} oof;
the
program may work, and it may not. If the programming environment builds the
program so that the first file is initialized before the second file, then
there will be no problem. However, if the second file is initialized before the
first, the constructor for
oof
relies upon the existence of
out,
which hasn’t been constructed yet and this causes chaos. This is only a
problem with static object initializers
that
depend on each other
,
because by the time you get into
main( ),
all constructors for static objects have already been called.
A
more subtle example can be found in the ARM.
[30]
In one file,
extern int y;
int x = y + 1;
extern int x;
int y = x + 1;
For
all static objects, the linking-loading mechanism guarantees a static
initialization to zero
before the dynamic initialization specified by the programmer takes place. In
the previous example, zeroing of the storage occupied by the
fstream
out
object has no special meaning, so it is truly undefined until the constructor
is called. However, with built-in types, initialization to zero
does
have meaning, and if the files are initialized in the order they are shown
above,
y
begins as statically initialized to zero, so
x
becomes one, and
y
is dynamically initialized to two. However, if the files are initialized in the
opposite order,
x
is statically initialized to zero,
y
is dynamically initialized to one, and
x
then becomes two.
Programmers
must be aware of this because they can create a program with static
initialization dependencies and get it working on one platform, but move it to
another compiling environment where it suddenly, mysteriously, doesn’t
work.
What
to do
There
are three approaches to dealing with this problem:
- Don’t
do it. Avoiding static initializer dependencies is the best solution.
- If
you must do it, put the critical static object definitions in a single file, so
you can portably control their initialization by putting them in the correct
order.
- If
you’re convinced it’s unavoidable to scatter static objects across
translation units – as in the case of a library, where you can’t
control the programmer who uses it – there is a technique pioneered by
Jerry Schwar
z
while creating the iostream library (because the definitions for
cin,
cout,
and
cerr
live in a separate file).
This
technique requires an additional class in your library header file. This class
is responsible for the dynamic initialization of your library’s static
objects. Here is a simple example:
//: C10:Depend.h
// Static initialization technique
#ifndef DEPEND_H
#define DEPEND_H
#include <iostream>
extern int x; // Declarations, not definitions
extern int y;
class Initializer {
static int init_count;
public:
Initializer() {
std::cout << "Initializer()" << std::endl;
// Initialize first time only
if(init_count++ == 0) {
std::cout << "performing initialization"
<< std::endl;
x = 100;
y = 200;
}
}
~Initializer() {
std::cout << "~Initializer()" << std::endl;
// Clean up last time only
if(--init_count == 0) {
std::cout << "performing cleanup"
<< std::endl;
// Any necessary cleanup here
}
}
};
// The following creates one object in each
// file where DEPEND.H is included, but that
// object is only visible within that file:
static Initializer init;
#endif // DEPEND_H ///:~
The
declarations for
x
and
y
announce only that these objects exist, but don’t allocate storage for
them. However, the definition for the
Initializer
init
allocates storage for that object in every file where the header is included,
but because the name is
static
(controlling visibility this time, not the way storage is allocated because
that is at file scope by default), it is only visible within that translation
unit, so the linker will not complain about multiple definition errors.
Here
is the file containing the definitions for
x,
y,
and
init_count:
//: C10:Depdefs.cpp {O}
// Definitions for DEPEND.H
#include "Depend.h"
// Static initialization will force
// all these values to zero:
int x;
int y;
int Initializer::init_count;
///:~
(Of
course, a file static instance of
init
is also placed in this file.) Suppose that two other files are created by the
library user:
//: C10:Depend.cpp {O}
// Static initialization
#include "Depend.h"
///:~
//: C10:Depend2.cpp
//{L} Depdefs Depend
// Static initialization
#include "Depend.h"
using namespace std;
int main() {
cout << "inside main()" << endl;
cout << "leaving main()" << endl;
} ///:~
Now
it doesn’t matter which translation unit is initialized first. The first
time a translation unit containing
Depend.h
is
initialized,
init_count
will be zero so the initialization will be performed. (This depends heavily on
the fact that global objects of built-in types are set to zero before any
dynamic initialization takes place.) For all the rest of the translation units,
the initialization will be skipped. Cleanup happens in the reverse order, and
~Initializer( )
ensures that it will happen only once.
This
example used built-in types as the global static objects. The technique also
works with classes, but those objects must then be dynamically initialized by
the
Initializer
class. One way to do this is to create the classes without constructors and
destructors, but instead with initialization and cleanup member functions using
different names. A more common approach, however, is to have pointers to
objects and to create them dynamically on the heap inside
Initializer( ).
This requires the use of two C++ keywords,
new
and
delete,
which will be explored in Chapter 11.
[30]Bjarne
Stroustrup and Margaret Ellis,
The
Annotated C++ Reference Manual
,
Addison-Wesley, 1990, pp. 20-21.
Go to CodeGuru.com
Contact: webmaster@codeguru.com
© Copyright 1997-1999 CodeGuru