Previous section   Next section

Imperfect C++ Practical Solutions for Real-Life Programming
By Matthew Wilson
Table of Contents
Chapter 11.  Statics


11.2. Singletons

The notion of the singleton pattern [Gamm1995] is that for a given type there exists only a single instance of it. In C++, the implementation of a singleton generally relies on a static object. Since C++ lets you shoot your leg off,[3] if a type relies on singleton semantics the author of the type should ensure that only a single instance can be created. There are several mechanisms available, all of which rely on the static keyword in one form or another.

[3] Bjarne Stroustrup is widely reputed to have said something like: "C makes it easy to shoot yourself in the foot; C++ makes it harder, but when you do it blows your whole leg off" [Stro-Web]. I think this book demonstrates that I'm in agreement with that sentiment.

11.2.1 Meyers Singleton

In [Meye1998] Scott Meyers describes what has come to be known as the Meyers Singleton. Basically, it looks like this:

Listing 11.5.


class Thing


{


public:


  Thing &GetInstance()


  {


    static Thing s_instance;


    return s_instance;


  }


private:


  Thing(Thing const &);


  Thing &operator =(Thing const &);


};



This uses lazy-evaluation [Meye1996] to create the single instance on demand, via the GetInstance() method. Hiding copy constructor and copy assignment operator ensure that no other instances can be made.

There are several problems with this. First, it uses local static objects, which we look at in detail in section 11.3, and therefore represents, in its current guise, a race condition in multithreaded execution.

Second, you have no control over the lifetime of the instance, beyond the fact that it is created the first time it is used, and will be destroyed during the process shutdown mechanisms, along with all other static objects (local and nonlocal). If something—probably another static object's destructor—calls Thing::GetInstance() after the destruction of s_instance, it will be talking to an uninitialized object, and bad things will happen. This is known as the Dead Reference problem [Alex2001].

11.2.2 Alexandrescu Singleton

A sophisticated solution to the dead-reference problem is provided in the form of the Alexandrescu Singleton [Alex2001], which hooks into the language cleanup mechanisms for singleton objects and ensures that they are destroyed according to a relative longevity rating.

Each type's single instance is created on the heap, and the logical nature of the type would be similar to the following:

Listing 11.6.


class Thing


{


public:


  Thing &GetInstance()


  {


    if(NULL == sm_instance) // The real one would be thread-safe


    {


      sm_instance = new Thing();


      SingletonInfrastructure::Register(sm_instance, . . .);


    }


    return *sm_instance;


  }


private:


  static Thing *sm_instance;


  . . .


};



The destruction of Thing is then scheduled in a manner according to a programmer-supplied grading of its "longevity." Although the actual implementation is quite complex, it would be churlish of me to shy away from complexity if it helps overcome an imperfection, and I'm sure you'd be the first to point that out, gentle reader.

Using this technique would actually result in your code containing something like the following:



class ThingImpl { . . .};


class AnotherImpl { . . .};


inline unsigned GetLongevity(ThingImpl *) { return 3; }


inline unsigned GetLongevity(AnotherImpl *) { return 1; }





typedef SingletonHolder<ThingImpl, . . .>     Thing;


typedef SingletonHolder<AnotherImpl, . . .>   Another;



The GetLongevity() functions provide a relative longevity, which the infrastructure then uses to arbitrate destruction ordering to ensure that Thing lives longer than Another.[4]

[4] These functions are actually attribute shims, which we meet in Chapter 20.

There are some really interesting and informative concepts bound up in this implementation, and I heartily recommend that you familiarize yourself with it, but the problem I have is that the solution is based on the programmer providing the ranking values for all global objects in the system. This seems to be easy to get wrong, and potentially hard to diagnose, since there's no reason that an erroneous ranking would show up within an arbitrary test sequence. Furthermore it could require a considerable amount of effort if new types/singletons need to be added into the mix. My solution, which we'll meet shortly, is much less sophisticated, but also requires less foresight from the programmer.[5]

[5] You might suggest that this may be a reflection of my own limitations; I couldn't possibly comment.

11.2.3 Just-in-Time Schwarz Counters: A Nifty Idea

There's a really simple solution to the global object ordering issue known as the Schwarz Counter—also called the "Nifty Counter"—which was described by Jerry Schwarz [Lipp1998], as a mechanism for ensuring that static objects within the IOStreams library are available before mainline execution begins, and before they are used by any other static objects.[6]

[6] This technique is one of the most widely "invented" in C++. Being more doer than reader, I had my own private Eureka! moment on this issue, only to subsequently realize that smarter people had been there before me.

We've seen an example of this already, in section 6.4, for ensuring timely new handler installation. The technique is to define an initializer class, and inject an instance of it into every compilation unit that includes the header of the class or API to initialize. When applied to the Thing class, it would look like the following:

Listing 11.7.


class Thing


{


public:


  Thing &GetInstance();


  . . .


};





struct ThingInit


{


  ThingInit()


  {


    Thing::GetInstance();


  }


};


static ThingInit s_thingInit;



Now the static[7] instance s_thingInit will acquire the instance in each compilation unit. The first one in gets the worm, and the assumption is that it will also be the last one to need it. The Schwarz counter works really well for independent static objects that merely serve other "client" objects or mainline code. However, there are still two problems with this approach.

[7] As I've said before (see sections 6.4 and 11.1.2), anonymous namespaces are the preferred mechanism of achieving internal linkage. I'm using static here because the code shown in the next section uses it. In any case, I'm not frightened of looking like a crusty old duffer.

First, if you are brave enough to be starting threads from within global object initialization, then it's possible to have a race condition in the GetInstance() method. In general it's not a desirable thing to be starting threads before you enter main(), for a variety of reasons, not the least of which is that its possible to deadlock the process since some run time libraries use dynamic library initialization functions, which are internally serialized, to activate global object constructors and provide thread attachment notifications.

Recommendation: Don't create threads during global object initialization.


The second problem is that it is still possible to have ordering problems. If the order of inclusion is different in different compilation units, it is still possible to get things created and destroyed in the wrong order. Say you have two classes A and B, in files headerA.h and headerB.h, that also contain initializer objects initA and initB, respectively. Now we have two implementation files X.cpp and Y.cpp, as follows:



// X.cpp


#include <headerA.h> // brings in initA


#include <headerB.h> // brings in initB





// Y.cpp


#include <headerB.h> // brings in initB


#include <headerA.h> // brings in initA



The designers of A and B have nicely used forward declarations, so if A depends on B, its header merely declares class B; rather than including headerB.h, and vice versa.

But, again, we are subject to the vagaries of linker order. If the objects in X.cpp are link ordered before those in Y.cpp, we will have the initialization order of initA, initB, initB, initA, which will create the A singleton before the B singleton. If the link ordering is reversed, the singleton creation is reversed.

Although this ordering fragility is rare in practice, it can still happen, so this solution is not good enough.

11.2.4 Counting on APIs

In C, the unit of functionality above the function is the library, as represented by the set of functions, or API, that provide access to it. Some libraries are simply a set of related, but unbound, functions. A good example of this would be the C standard library's string functions.[8] We can call these stateless libraries.

[8] With one or two minor exceptions, e.g., strtok().

Other libraries, however, maintain internal state that supports some or all of their functions. These are stateful libraries. We saw an example of this with the Tss library from section 10.5.

A library may itself be stateless, but might use other stateful libraries for the implementation of its functionality. Since it will rely, indirectly, on the state of the underlying libraries, we'll regard them as stateful for the purposes of this discussion.

It is a requirement of stateful libraries that they be initialized prior to their use, and uninitialized once they are no longer needed. This initialization can take several forms. It may be that the initialization function should be called once only. This would usually be done within the process main(). Another form may be that the initialization function ignores all but the first call. Either of these forms may come with an uninitialization function, but that's not always the case.

Both of these forms represent a problem when used in applications consisting of multiple link units. A better form allows multiple calls of the initialization function, and a corresponding collection of multiple calls of the uninitialization function—so-called reference-counting APIs.

Although it's not often discussed, all general purpose, and most specific, libraries should be able to be multiply initialized/uninitialized, such that each component of client code within a greater whole can be made as independent as possible. The unappealing alternative is having to rely on the author of the applications within which they reside to provide the initialization for them.

As long as clients of such APIs faithfully initialize and uninitialize them correctly, and as long as the APIs reference count the APIs that they themselves use, then it all works without a hitch.

For example, the initialization and uninitialization functions for the Tss library (see section 10.5) look like the following (with error-handling elided):[9]

[9] I can't show you the unprintable contents of _sg_tsslib() here, but it's on the CD if you really must torture yourself.

Listing 11.8.


int Tss_Init(void)


{


  CoreSync_Init();  // Initialise synch objects API


  Mem_Init();       // Initialise the memory API


  {


    Scope_CoreSync  lock(sg_cs);


    _sg_tsslib(1); // Create the tss singleton


  }


  return 0;


}


void Tss_Uninit(void)


{


  {


    Scope_CoreSync  lock(sg_cs);


    _sg_tsslib(-1); // Close the tss singleton


  }


  Mem_Uninit();


  CoreSync_Uninit();


}



Thus the Tss library cannot possibly be used when the libraries on which it depends—CoreSync and Mem—are not initialized. So the ordering problem does not raise its ugly head.

A great side effect of this approach is that any cyclic interdependencies are detected immediately, since the initialization will go into an infinite loop. Although we don't like infinite loops, they are very useful if their occurrence is deterministic, as it is in this case. If your initialization function returns to you, you have no cyclic interdependencies; if it does not, then your code's not ready to ship.

There's one final minor point. Some libraries require that the first call to the initialization function be (manually) serialized, that is, the user of the library must ensure that the first call is made from the main thread before any other threads have a chance of calling it. This is usually because the synchronization primitives used to ensure thread-safe access are themselves created within the first call. While in most cases this does not represent a problem, it is self-evident that libraries that can handle multithreaded initialization are more robust and easier to use.

11.2.5 Counted APIs, Wrappers, Proxies: Ordered Singletons at Last!

This whole issue bugged me for several years, until I realized that taking what some might term a step back actually represents a (simple) step forward. I recognize that I may be accused of teaching grandma to suck eggs by C programmers, or of being anachronistic by C++ programmers, but I'm afraid a solution is all I want here, and that's what we've got. Simply put, a multiply-initializable stateful library is logically equivalent to a singleton. Given that we've seen that we can provide correct ordering for such libraries, it's but a small leap to represent ordered singletons as thin wrappers over them. All that needs to be done is to throw a little Schwarz counter and #ifdef exclusion into the mix, and we're there.

Using the Tss library as an example, the singleton form would look like Listing 11.9.

Listing 11.9.


class Tss


{


public:


  Tss()


  {


    if(Tss_Init() < 0)


    {


      . . . // Throw an appropriate exception


    }


  }


  ~Tss()


  {


    Tss_Uninit();


  }


  HTssKey CreateKey( void (*pfnClose)()


                   , void (*pfnClientConnect)()


                   , void (*pfnClientDisconnect)()


                   , Boolean bCloseOnAssign = false);


  . . . // other Tss API functions as methods


private:


  . . . Hide copy ctor and assignment operator


};


#ifndef ACMELIB_BUILDING_TSS_API


static Tss  tss; // The Schwarz counter


#endif /* ACMELIB_BUILDING_TSS_API */



First, each compilation unit that includes the header gets the Schwarz counter, and so every compilation unit that needs it is guaranteed that the Tss "singleton" exists. Second, the ordering dependencies between the "singletons" is handled within the Init/Uninit functions of the APIs—any library that itself relies on the presence of the Tss library will call Tss_Init/Unit within its own initialization functions; that's the dead reference problem handled. Finally, the Schwarz counter is elided from the compilation unit(s) in which the Tss library itself resides, since we don't want it to have a dependency on itself.

This technique discards, at least in plain form, the lazy evaluation of most C++-based singleton techniques. However, it is pretty simple to dissociate the initialization of the API from the creation of the underlying state, and lazily create the state on demand. Since it is the API, and not the instance, that is reference counted, the creation of the state can be deferred until needed but yet deterministically destroyed when the API reference count goes to 0.

There's an implicit assumption in all this, which is that the library state can be resurrected if, after an uninitialization, it can be reinitialized. In practice, I've never encountered any problems in making this possible, or in having it happen, but it's only fair to mention it, since you may run into a situation in which that's not feasible and/or desirable.

The only real penalty of this approach is that there is an efficiency cost in the use of the singleton. It's very likely that there'll always be one function call per singleton method call that is not inlined (except where using whole-program optimizations). However, since a singleton is usually a big thing, doing big important things, this is probably not going to be noticeable. In any case, correctness is more important than efficiency.

Whether you choose to use simple wrappers over the library API, or whether you incorporate the portable vtables technique to actually be manipulating bona fide C++ objects is up to you; in either case you're working with a singleton!


      Previous section   Next section