Technique two
Long after technique one was in use,
someone (I don’t know who) came up with the technique explained in this
section, which is much simpler and cleaner than technique one. The fact that it
took so long to discover is a tribute to the complexity of C++.
This technique relies on the fact that
static
objects inside functions are initialized the first time (only) that the function
is called. Keep in mind that the problem we’re really trying to solve here
is not when the static objects are initialized (that can be controlled
separately) but rather making sure that the initialization happens in the proper
order.
This technique is very neat and clever.
For any initialization dependency, you place a static object inside a function
that returns a reference to that object. This way, the only way you can access
the static object is by calling the function, and if that object needs to access
other static objects on which it is dependent it must call their
functions. And the first time a function is called, it forces the initialization
to take place. The order of static initialization is guaranteed to be correct
because of the design of the code, not because of an arbitrary order established
by the linker.
To set up an example, here are two
classes that depend on each other. The first one contains a bool that is
initialized only by the constructor, so you can tell if the constructor has been
called for a static instance of the class (the static storage area is
initialized to zero at program startup, which produces a false value for
the bool if the constructor has not been called):
//: C10:Dependency1.h
#ifndef DEPENDENCY1_H
#define DEPENDENCY1_H
#include <iostream>
class Dependency1 {
bool init;
public:
Dependency1() : init(true) {
std::cout << "Dependency1 construction"
<< std::endl;
}
void print() const {
std::cout << "Dependency1 init: "
<< init << std::endl;
}
};
#endif // DEPENDENCY1_H ///:~
The constructor also announces when it is
being called, and you can print( ) the state of the object to find
out if it has been initialized.
The second class is initialized from an
object of the first class, which is what will cause the
dependency:
//: C10:Dependency2.h
#ifndef DEPENDENCY2_H
#define DEPENDENCY2_H
#include "Dependency1.h"
class Dependency2 {
Dependency1 d1;
public:
Dependency2(const Dependency1& dep1): d1(dep1){
std::cout << "Dependency2 construction ";
print();
}
void print() const { d1.print(); }
};
#endif // DEPENDENCY2_H ///:~
The constructor announces itself and
prints the state of the d1 object so you can see if it has been
initialized by the time the constructor is called.
To demonstrate what can go wrong, the
following file first puts the static object definitions in the wrong order, as
they would occur if the linker happened to initialize the Dependency2
object before the Dependency1 object. Then the order is reversed to show
how it works correctly if the order happens to be “right.” Lastly,
technique two is demonstrated.
To provide more readable output, the
function separator( ) is created. The trick is that you can’t
call a function globally unless that function is being used to perform the
initialization of a variable, so separator( ) returns a dummy value
that is used to initialize a couple of global variables.
//: C10:Technique2.cpp
#include "Dependency2.h"
using namespace std;
// Returns a value so it can be called as
// a global initializer:
int separator() {
cout << "---------------------" << endl;
return 1;
}
// Simulate the dependency problem:
extern Dependency1 dep1;
Dependency2 dep2(dep1);
Dependency1 dep1;
int x1 = separator();
// But if it happens in this order it works OK:
Dependency1 dep1b;
Dependency2 dep2b(dep1b);
int x2 = separator();
// Wrapping static objects in functions succeeds
Dependency1& d1() {
static Dependency1 dep1;
return dep1;
}
Dependency2& d2() {
static Dependency2 dep2(d1());
return dep2;
}
int main() {
Dependency2& dep2 = d2();
} ///:~
The functions d1( ) and
d2( ) wrap static instances of Dependency1 and
Dependency2 objects. Now, the only way you can get to the static objects
is by calling the functions and that forces static initialization on the first
function call. This means that initialization is guaranteed to be correct, which
you’ll see when you run the program and look at the
output.
Here’s how you would actually
organize the code to use the technique. Ordinarily, the static objects would be
defined in separate files (because you’re forced to for some reason;
remember that defining the static objects in separate files is what causes the
problem), so instead you define the wrapping functions in separate files. But
they’ll need to be declared in header files:
//: C10:Dependency1StatFun.h
#ifndef DEPENDENCY1STATFUN_H
#define DEPENDENCY1STATFUN_H
#include "Dependency1.h"
extern Dependency1& d1();
#endif // DEPENDENCY1STATFUN_H ///:~
Actually, the “extern” is
redundant for the function declaration. Here’s the second header
file:
//: C10:Dependency2StatFun.h
#ifndef DEPENDENCY2STATFUN_H
#define DEPENDENCY2STATFUN_H
#include "Dependency2.h"
extern Dependency2& d2();
#endif // DEPENDENCY2STATFUN_H ///:~
Now, in the implementation files where
you would previously have placed the static object definitions, you instead
place the wrapping function definitions:
//: C10:Dependency1StatFun.cpp {O}
#include "Dependency1StatFun.h"
Dependency1& d1() {
static Dependency1 dep1;
return dep1;
} ///:~
Presumably, other code might also be
placed in these files. Here’s the other file:
//: C10:Dependency2StatFun.cpp {O}
#include "Dependency1StatFun.h"
#include "Dependency2StatFun.h"
Dependency2& d2() {
static Dependency2 dep2(d1());
return dep2;
} ///:~
So now there are two files that could be
linked in any order and if they contained ordinary static objects could produce
any order of initialization. But since they contain the wrapping functions,
there’s no threat of incorrect initialization:
//: C10:Technique2b.cpp
//{L} Dependency1StatFun Dependency2StatFun
#include "Dependency2StatFun.h"
int main() { d2(); } ///:~
When you run this program you’ll
see that the initialization of the Dependency1 static object always
happens before the initialization of the Dependency2 static object. You
can also see that this is a much simpler approach than technique
one.
You might be tempted to write
d1( ) and d2( ) as inline functions inside their
respective header files, but this is something you must definitely not do. An
inline function can be duplicated in every file in which it appears – and
this duplication includes the static object definition. Because inline
functions automatically default to internal linkage, this would result in having
multiple static objects across the various translation units, which would
certainly cause problems. So you must ensure that there is only one definition
of each wrapping function, and this means not making the wrapping functions
inline.