I l@ve RuBoard |
![]() ![]() |
6.10 Putting It All TogetherThis 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 PoliciesLet'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:
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 PoliciesLet'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:
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 SingletonHolderNow 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:
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 ImplementationsDecomposition 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.
What remains is only to figure out how to use and extend this small yet mighty SingletonHolder template. ![]() |
I l@ve RuBoard |
![]() ![]() |