I l@ve RuBoard Previous Section Next Section

6.10 Putting It All Together

This chapter has wandered around various possible implementations of Singleton, commenting on their relative strengths and weaknesses. The discussion doesn't lead to a unique implementation because the problem at hand is what dictates the best Singleton implementation.

The SingletonHolder class template defined by Loki is a Singleton container that assists you in using the Singleton design pattern. Following a policy-based design (see Chapter 1), SingletonHolder is a specialized container for a user-defined Singleton object. When using SingletonHolder, you pick the features needed, and maybe provide some code of your own. In extreme cases, you might still need to start all over again—which is okay, as long as those are indeed extreme cases.

This chapter visited quite a few issues that are mostly independent of each other. How, then, can Singleton implement so many cases without utter code bloating? The key is to decompose Singleton carefully into policies, as discussed in Chapter 1. By decomposing SingletonHolder into several policies, we can implement all the cases discussed previously in a small number of lines of code. By using template instantiation, you select the features needed and don't pay anything for the unneeded ones. This is important: The implementation of Singleton put together here is not the do-it-all class. Only the features used are ultimately included in the generated code. Plus, the implementation leaves room for tweaks and extensions.

6.10.1 Decomposing SingletonHolder into Policies

Let's start by delimiting what policies we can distinguish for the implementations discussed. We identified creation issues, lifetime issues, and threading issues. These are the three most important aspects of Singleton development. The three corresponding polices therefore are as follows:

  1. Creation. You can create a singleton in various ways. Typically, the Creation policy invokes the new operator to create an object. Isolating creation as a policy is essential because it enables you to create polymorphic objects.

  2. Lifetime. We identified the following lifetime policies:

    1. Following C++ rules—last created, first destroyed

    2. Recurring (Phoenix singleton)

    3. User controlled (singleton with longevity)

    4. Infinite (the "leaking" singleton, an object that's never destroyed)

  3. ThreadingModel. Whether singleton is single threaded, is standard multithreaded (with a mutex and the Double-Checked Locking pattern), or uses a nonportable threading model.

All Singleton implementations must take the same precautions for enforcing uniqueness. These are not policies, because changing them would break the definition of Singleton.

6.10.2 Defining Requirements for SingletonHolder's Policies

Let's define the necessary requirements that SingletonHolder imposes on its policies.

The Creation policy must create and destroy objects, so it must expose two corresponding functions. Therefore, assuming Creator<T> is a class compliant with the Creation policy, Creator<T> must support the following calls:



T* pObj = Creator<T>::Create();


Creator<T>::Destroy(pObj);


Notice that Create and Destroy must be two static members of Creator. Singleton does not hold a Creator object—this would perpetuate the Singleton lifetime issues.

The Lifetime policy essentially must schedule the destruction of the Singleton object created by the Creation policy. In essence, Lifetime policy's functionality boils down to its ability to destroy the Singleton object at a specific time during the lifetime of the application. In addition, Lifetime decides the action to be taken if the application violates the lifetime rules of the Singleton object. Hence:

  • If you need the singleton to be destroyed according to C++ rules, then Lifetime uses a mechanism similar to atexit.

  • For the Phoenix singleton, Lifetime still uses an atexit-like mechanism but accepts the re-creation of the Singleton object.

  • For a singleton with longevity, Lifetime issues a call to SetLongevity, described in Sections 6.7 and 6.8.

  • For infinite lifetime, Lifetime does not take any action.

In conclusion, the Lifetime policy prescribes two functions: ScheduleDestruction, which takes care of setting the appropriate time for destruction, and OnDeadReference, which establishes the behavior in case of dead-reference detection.

If Lifetime<T> is a class that implements the Lifetime policy, the following expressions make sense:



void (*pDestructionFunction)();


...


Lifetime<T>::ScheduleDestruction(pDestructionFunction);


Lifetime<T>::OnDeadReference();


The ScheduleDestruction member function accepts a pointer to the actual function that performs the destruction. This way, we can compound the Lifetime policy with the Creation policy. Don't forget that Lifetime is not concerned with the destruction method, which is Creation's charter; the only preoccupation of Lifetime is timing—that is, when the destruction will occur.

OnDeadReference throws an exception in all cases except for the Phoenix singleton, a case in which it does nothing.

The ThreadingModel policy is the one described in the appendix. SingletonHolder does not support object-level locking, only class-level locking. This is because you have only one object anyway.

6.10.3 Assembling SingletonHolder

Now let's begin defining the SingletonHolder class template. As discussed in Chapter 1, each policy mandates one template parameter. In addition to it, we prepend a template parameter (T) that's the type for which we provide singleton behavior. The SingletonHolder class template is not itself a Singleton. SingletonHolder provides only singleton behavior and management over an existing class.



template


<


   class T,


   template <class> class CreationPolicy = CreateUsingNew,


   template <class> class LifetimePolicy = DefaultLifetime,


   template <class> class ThreadingModel = SingleThreaded


>


class SingletonHolder


{


public:


   static T& Instance();


private:


   // Helpers


   static void DestroySingleton();


   // Protection


   SingletonHolder();


   ...


   // Data


   typedef ThreadingModel<T>::VolatileType InstanceType;


   static InstanceType* pInstance_;


   static bool destroyed_;


};


The type of the instance variable is not T* as you would expect. Instead, it's ThreadingModel<T>::VolatileType*. The ThreadingModel<T>::VolatileType type definition expands either to T or to volatile T, depending on the actual threading model. The volatile qualifier applied to a type tells the compiler that values of that type might be changed by multiple threads. Knowing this, the compiler avoids some optimizations (such as keeping values in its internal registers) that would make multithreaded code run erratically. A safe decision would be then to define pInstance_ to be type volatile T*: It works with multithreaded code (subject to your checking your compiler's documentation) and doesn't hurt in single-threaded code.

On the other hand, in a single-threaded model, you do want to take advantage of those optimizations, so T* would be the best type for pInstance_. That's why the actual type of pInstance_ is decided by the ThreadingModel policy. If ThreadingModel is a single-threaded policy, it simply defines VolatileType as follows:



template <class T> class SingleThreaded


{


   ...


public:


   typedef T VolatileType;


};


A multithreaded policy would have a definition that qualifies T with volatile. See the appendix for more details on threading models.

Let's now define the Instance member function, which wires together the three policies.



template <...>


T& SingletonHolder<...>::Instance()


{


   if (!pInstance_)


   {


      typename ThreadingModel<T>::Lock guard;


      if (!pInstance_)


      {


         if (destroyed_)


         {


            LifetimePolicy<T>::OnDeadReference();


            destroyed_ = false;


         }


         pInstance_ = CreationPolicy<T>::Create();


         LifetimePolicy<T>::ScheduleCall(&DestroySingleton);


      }


   }


   return *pInstance_;


}


Instance is the one and only public function exposed by SingletonHolder. Instance implements a shell over CreationPolicy, LifetimePolicy, and ThreadingModel. The ThreadingModel<T> policy class exposes an inner class Lock. For the lifetime of a Lock object, all other threads trying to create objects of type Lock will block. (Refer to the appendix.)

DestroySingleton simply destroys the Singleton object, cleans up the allocated memory, and sets destroyed_ to true. SingletonHolder never calls DestroySingleton; it only passes its address to LifetimePolicy<T>::ScheduleDestruction.



template <...>


void SingletonHolder<...>::DestroySingleton()


{


   assert(!destroyed_);


   CreationPolicy<T>::Destroy(pInstance_);


   pInstance_ = 0;


   destroyed_ = true;


}


SingletonHolder passes pInstance_ and the address of DestroySingleton to LifetimePolicy<T>. The intent is to give LifetimePolicy enough information to implement the known behaviors: C++ rules, recurring (Phoenix singleton), user controlled (singleton with longevity), and infinite. Here's how:

  • Per C++ rules. LifetimePolicy<T>::ScheduleDestruction calls atexit, passing it the address of DestroySingleton. OnDeadReference throws a std::logic_error exception.

  • Recurring. Same as above, except that OnDeadReference does not throw an exception. SingletonHolder's flow of execution continues and will re-create the object.

  • User controlled. LifetimePolicy<T>::ScheduleDestruction calls SetLongevity(GetLongevity(pInstance)).

  • Infinite. LifetimePolicy<T>::ScheduleDestruction has an empty implementation.

SingletonHolder handles the dead-reference problem as the responsibility of LifetimePolicy. It's very simple: If SingletonHolder::Instance detects a dead reference, it calls LifetimePolicy::OnDeadReference. If OnDeadReference returns, Instance continues with re-creating a new instance. In conclusion, OnDeadReference should throw an exception or terminate the program if you do not want Phoenix Singleton behavior. For a Phoenix singleton, OnDeadReference does nothing.

Well, that is the whole SingletonHolder implementation. Of course, now much work gets delegated to the three policies.

6.10.4 Stock Policy Implementations

Decomposition into policies is hard. After you do that, the policies are easy to implement. Let's collect the policy classes that implement the common types of singletons. Table 6.1 shows the predefined policy classes for SingletonHolder. The policy classes in bold are the default template parameters.

Table 6.1. Predefined Policies for SingletonHolder
Policy Predefined Class Template Comment
Creation CreateUsingNew Creates an object by using the new operator and the default constructor.
  CreateUsingMalloc Creates an object by using std::malloc and its default constructor.
  CreateStatic Creates an object in static memory.
Lifetime DefaultLifetime Manages the object's lifetime by obeying C++ rules. Uses atexit to get the job done.
  PhoenixSingleton Same as DefaultLifetime but allows re-creation of the Singleton object.
  SingletonWithLongevity Assigns longevity to the Singleton object. Assumes the existence of a namespace-level function GetLongevity that, called with pInstance_, returns the longevity of the Singleton object.
  NoDestroy Does not destroy the Singleton object.
ThreadingModel SingleThreaded See the appendix for details about threading models.
  ClassLevelLockable  

What remains is only to figure out how to use and extend this small yet mighty SingletonHolder template.

    I l@ve RuBoard Previous Section Next Section