Global Dependencies Made Easy
Jason Hise, March 28, 2007
Introduction
We've heard before that global variables are a Bad Thing. We've heard
that they are a breach of modularization, and that even when encapsulated
as singletons they leave a dark mark on your code and tend to attract
evil demons. Still, encapsulated as singletons or not, global variables
of some form often find their way into large projects. This article is
not about the pros or cons of globally accessible objects. It is about
making your life easier if you choose to or are forced to use them.
The Ordering Problem
A popular notion is that if you do want to use global objects, they are
incredibly hard to get right. The reason that I most often see cited (at
least for single threaded applications) is that the lifetimes of global
objects are hard to control, and therefore dependencies between globals
are dangerous. While mostly accurate when applied to variables defined
at file or namespace scope, this theory is not really true when it comes
to function local statics.
The big difference between a global variable and a function local static
is that the global variable is initialized before main starts executing,
and the static variable is not initialized until execution flow passes
through its declaration. The lazy initialization provided by the latter
gives us a strong degree of control regarding the order in which our
objects are created. Now we just need a way to control the order of
destruction.
The Big Secret
As it turns out, as soon as we take control of the creation order of
global objects, we are already in control of the order of destruction.
According to section 3.6.3 of the 1998 C++ Standard:
"Destructors for initialized objects of static storage duration
(declared at block scope or at namespace scope) are called as a result
of returning from main and as a result of calling exit. These
objects are destroyed in the reverse order of the completion of their
constructor or of the completion of their dynamic initialization.
"
This gives us all the control we need, and forms the foundation for
developing a singleton model that supports natural and safe
dependencies.
The Meyers Singleton
Let's start with the Meyers Singleton and build from there. For those
unfamiliar, this code structure is an incredibly elegant and compact way
of encapsulating and providing access to a global object. It is called
the Meyers Singleton because it was originally proposed by
Scott Meyers.
foo & get_the_foo()
{
static foo f;
return f;
}
This is simply a function which contains a local static variable of the
desired type and returns it by reference. The instance is only
constructed after the function is entered for the first time. This
provides the benefit of lazy creation, meaning that the instance is
created on demand, and is never initialized if it is never used. If
created, the instance will be destroyed upon program completion.
Introducing Dependencies
Now say we have two global objects. One is a simple logger, and the
other encapsulates a database connection. We want the database
connection to write to the logger both when the connection is being
established and when it is being torn down. In accordance with RAII,
it is desirable that tear down of the connection be performed
automatically in the destructor of the connection object. However, we
need to know that the global logger is available at that time and has not
already been destroyed.
So long as the connection uses the logger in its constructor, the problem
is already solved. If the logger is used before the connection, then
according to the standard it will be destroyed after the connection
(assuming the connection is used at all). If the connection is used
first instead, part of its initialization will cause the logger to be
created. Initialization of the logger will still finish before
initialization of the connection is complete. This means the logger
will always outlive the connection.
A More Generic Approach
Most of the time, if one global object needs to access another during
destruction it will require similar access during construction. But this
is not always the case, and passively depending on global side effects
between two independent functions is asking for trouble. So what would
really be nice is a more declarative method to define which objects
depend upon others. It turns out this is not too difficult to manage, so
long as we adopt a uniform convention for naming the instance functions
of our singletons. The first step is to define a templated struct to do
the dependency management. The only thing that this type is responsible
for is accessing the instance function of the specified type upon
construction.
template <typename Type>
struct depends_on
{
depends_on()
{
Type::instance();
}
};
Next, we jump right into defining the singletons we want. First we
implement a very trivial logger, which just provides the required
instance function and the ability to write to a file. The copy
constructor, assignment operator, and destructor could optionally be
provided in the private section to ensure they aren't used by client
code.
class logger
{
private:
ofstream out;
logger() : out("log.txt") { }
public:
static logger & instance()
{
static logger inst;
return inst;
}
void log(const char * msg)
{
out << msg << endl;
}
};
Now we can start work on the connection. All we need to do to declare
the dependency on the logger is to add depends_on<logger> as a base
class, and it becomes safe to use in the destructor. Check it out:
class connection
: depends_on<logger>
{
private:
~connection()
{ logger::instance()
.log("Connection closed.");
}
public:
static connection & instance()
{
static connection inst;
return inst;
}
};
Because base classes are completely initialized before the objects that
contain them, this technique gives us the same guarantees that using
the logger directly from the constructor of the connection would have.
Better yet, the dependency is now self documenting, and saves us having
to provide potentially redundant code if multiple connection
constructors are available.
Summary
As it turns out, the C++ Standard says a lot more than expected about
controlling the order of destruction of global objects. With very little
overhead, it can be incredibly easy to write safe, correct, and self
documenting code in which some global objects depend upon others. Though
there will most likely continue to be debates about the merits of using
the singleton pattern, this technique will hopefully make it less painful
to manage global objects if or when the need arises.