Singletons have to be destroyed, just like any other objects. When and how that happens can be a tricky problem to solve.
Introduction
The Singleton object creational pattern presented by the GOF ("Gang of Four") in Design Patterns [1] is a superior implementation of the notion of global objects. Its major benefits include:
1. It limits the number of class instances that can be created.
2. Instances are created only if needed.
3. The instance is always created prior to being referenced. (This effectively solves the problem of initialization order when several interdependent instances are involved.)
The sample code provided in Design Patterns ignores the problem of global objects' destruction and relies on the operating system to release the memory they occupy. The resulting memory leak may not be so damaging, since the operating system reclaims the dynamically allocated memory when the process terminates. But if the destructors release system-wide resources, things can get pretty messy if the resource allocation and deallocation do not follow a disciplined pattern.
In Effective C++ [2], Scott Meyers suggests an alternative singleton implementation to the one suggested by the GOF. In Meyers' implementation, the object instance is defined static in a dedicated function, which returns a reference to the instance. This alternative implementation invokes the singleton destructor at program termination, thus preventing memory and resource leaks. However, this implementation also tightly binds the singleton object to the enclosing ("wrapping") function.
Regardless of the number of calls to the singleton's constructing method, only one instance of the class is created. Since all the clients of the class share this instance, the design should impose a controlled protocol regarding the deletion of this object. Although a number of efforts have been made to formalize this approach [3], none address the issue of destruction order in the presence of several singleton objects that depend on each other.
Wrapping globally available objects in functions solves the problem of mutual initialization order, but what happens if the destruction order also matters? Suppose there is a global Logger object, and destructors of other global entities must record with Logger their released resources. Obviously, Logger should be destroyed last. If the language rules destroy the Logger first, other objects might unknowingly attempt to use it, leading to unpredictable (and most likely disastrous) consequences.
Figure 1 shows an alternative Singleton implementation, which will later serve as the basis for a variation with certain additional properties. The proposed solution has the following important features:
- The Standard C++ library [4] implementation of the smart pointer (the auto_ptr class template) performs the singleton's resource management. auto_ptr owns the object pointed to by its data member and is responsible for its deletion. To facilitate this scheme, an auxiliary private function of class Singleton (get_instance) defines a static auto_ptr to the actual object. In fact, the auto_ptr serves as a "proxy" for the object. The object instance owned by the auto_ptr is detached from the function wrapper (instance) that ensures its singleton properties. This solution does not sacrifice proper memory management. Instead, it allows real objects to be destroyed in an order other than the one induced by C++ language rules, thus facilitating alternative destruction policies.
- If auto_ptr actually deletes the object it owns, it needs access to that object's destructor, hence the friendship between class Singleton and auto_ptr<Singleton>.
- The single instance of auto_ptr is implemented as a static variable in the method Singleton::get_instance. auto_ptr in turn controls a single instance of the singleton object, which is created on the heap (using new). This provides a clean solution to the initialization order problem, which would have occurred if auto_ptr were created as a static data member of the class.
In Design Patterns, the GOF make the pointer to the singleton instance a static data member of the class, but their implementation uses a regular pointer, rather than an auto_ptr. A C++ built-in pointer has no constructor, and the Standard guarantees that a global pointer is initialized to zero. But if the auto_ptr is made a static data member of the class, then the auto_ptr object (not the object it owns) will be created at the global scope. The C++ Standard does not define the initialization order of global objects unless they are all defined within the same translation unit. Hence if another global object, whose constructor needs a singleton, is created first, then the method Singleton::get_instance will be invoked. Since there is no guarantee that auto_ptr's constructor has already been called, this may result in the disastrous consequence of dereferencing a garbage value as if it were a valid pointer.
- Function get_instance is not defined inline, but rather in the implementation file. The new C++ Standard [4] states that "a static local variable in a member function always refers to the same object, whether or not the member function is inline." Nevertheless, old compilers may still be in use that implement old language rules. Under these rules, each translation unit that includes the definition of such a function contains its own static copy of this function with its own copy of the variables. This issue is explained in detail in [2].
- Two public functions provide const and non-const access to the singleton by dereferencing auto_ptr. For additional code safety, if the constant object will do the job, the corresponding function const_instance should be used.
- The access functions return a reference to the actual object (not to auto_ptr), in order to conceal the implementation details from the clients.
The Singleton Destruction Manager
I return to the example in which several global objects depend on one another and consequently cannot be destroyed in an arbitrary order. If some global object is a client of another global object, the latter may not be destroyed until the former terminates. Otherwise, if the former inadvertently invokes a function of the latter after it ceases to exist, the aftermath may be rather gloomy.
I base my solution on the composite Singleton-Registration-Proxy pattern. I use a dedicated Destruction Manager (implemented as a singleton) to control the global object destruction order. When a singleton object is created, its constructor registers with the Destruction Manager, specifying when this object should be destroyed (relative to other singletons).
All the singletons in the program are assigned a "destruction phase" figure, so that the smaller the phase, the later the object should be destroyed. (I assume that there are no circular dependencies between the objects, so it is possible to assign each singleton an appropriate destruction phase.) Each singleton constructor creates a destructor object, which encapsulates a pointer to the singleton and its destruction phase. At the end of main, the programmer invokes a dedicated function of the Destruction Manager. This function sorts all the destructor objects registered with the Destruction Manager in decreasing order of their phases and then destroys the singletons in that order. Each singleton's destruction is performed by releasing the singleton pointer from the auto_ptr that encloses it and then deleting that pointer. If the Destruction Manager does not destroy a singleton (for example, the destruction manager is also a singleton, but does not destroy itself), the auto_ptr mechanism destroys the singleton object at the program's end. (Recall that the auto_ptr is defined as a local static variable in a function. Local static objects are destroyed at program termination.)
In Figures 2-6, I show the gist of my solution. The complete source code is available from the CUJ ftp site (see p. 3 for downloading instructions).
Figure 2 defines the DestructionPhase class. Each DestructionPhase object can be compared to other DestructionPhase objects using operator>.
Figure 3 shows the class hierarchy for destructor objects. I first define an abstract Destructor class, whose derived-class instances can be sorted according to their destruction phases. Class template TDestructor derives from Destructor and contains a pointer to the singleton object it is responsible for. The template parameter will be specialized for an actual singleton class, which must define a destroy_instance function that will destroy its own instance. (Note: the Singleton class shown in Figure 1 does not have a destroy_instance function. This class is for illustration only and will not work with the Destruction Manager.) An example of destroy_instance appears in Figure 5. It obtains a reference to the auto_ptr that owns the singleton instance through the singleton's get_instance method and deletes the object owned by auto_ptr. (This shows why auto_ptr is necessary; it detaches the object from its get_instance function wrapper.)
Figure 4 outlines the Destruction Manager. (Some obvious details have been omitted for brevity's sake). Singletons register their destructors with the Destruction Manager via function register_destructor. The Destruction Manager is responsible for the memory occupied by Destructor objects; therefore, it has to delete these objects before it terminates.
Function destroy_objects sorts the destructors in decreasing order of their phases and then consecutively destroys the singleton objects they manage. This function should be manually invoked at the end of main to ensure proper destruction order of the program's global objects. Class template greater_ptr, an auxiliary predicate, compares objects based on their pointers.
Figure 5 gives a sample resource definition, a Logger class. (Again, insignificant details have been omitted.) When invoked, the Logger constructor creates a new TDestructor object, which contains all the information necessary to destroy the Logger object at the right time. The TDestructor's constructor registers the destructor object with the Destruction Manager. When it is time for the Destruction Manager to destroy the Logger singleton, it calls the TDestructor's destroy method, which in turn invokes the Logger's destroy_instance method. Function destroy_instance releases the singleton object from the auto_ptr and then destroys it. The auto_ptr no longer owns the object; therefore, it will not attempt to delete it when the program terminates. Since the TDestructor object must invoke destroy_instance, the TDestructor class is defined as a friend of class Logger. After the singleton is deleted, the Destruction Manager ultimately deallocates the TDestructor object itself.
The complete source code also defines class Resource, whose destructor uses function Logger::log to record error messages (if any).
Finally, Figure 6 shows function main.
Possible Alternatives
Instead of using an express notion of destruction phases, an alternative approach could require each singleton to explicitly specify its dependency on other objects. The destruction manager would then perform a topological sort of the dependencies graph, thus deducing the destruction phases automatically. This would require the constructor of each singleton to inform the Destruction Manager of all the other singletons upon which it depends. In a simple solution where the constructor registers pointers to all the other singletons it might use, all those singletons would be created even if some of them were not needed in a particular program execution. A more elaborate solution would identify singletons by their string names rather than by pointers, but this seems like overkill.
Conclusion
In this article, I have examined the existing singleton implementations and have analyzed their advantages and drawbacks. I have also proposed an improved Singleton implementation, which detaches the singleton object from its function wrapper, and then have combined that implementation with other design patterns to achieve a certain behavior.
Using the compound Singleton-Registration-Proxy pattern, I have implemented a Destruction Manager, which controls the lifetime of a singleton and ensures that the given singleton exists as long as it is needed. In fact, it is possible to view the Destruction Manager as a "destructional pattern," as opposed to creational patterns [1] like Abstract Factory, Factory Method, or even Singleton, for that matter. These types of patterns lie on opposite side of the lifecycle-management spectrum. Certainly, additional research should be conducted (including documenting other known uses) before pronouncing the Destruction Manager a pattern in its own right.
This article does not address thread-safety. To my knowledge, each available Singleton implementation uses variables defined at global scope. In a multithreaded environment, this may constitute a problem, should a singleton have to serve multiple threads. Mutual exclusion in the presence of threads is a fairly well researched topic, but its application to the Singleton pattern may be non-trivial and thus deserves a detailed examination.
Acknowledgements
The Singleton Destruction Manager was inspired by example code from [5], where an initialization manager working in phases was used to resolve the mutual initialization order of global variables (using two-stage object construction instead of singletons).
Thanks to Brad Appleton for interesting discussions that helped to improve this article and to Avner Ben and Vitaly Surazhsky for their constructive comments.
References
[1] Gamma, E., et al. Design Patterns: Elements of Reusable Software Architecture (Addison Wesley, 1995).
[2] Meyers, S. Effective C++, 2nd edition (Addison Wesley, 1998), Item 47.
[3] Vlissides, J. Pattern Hatching. Design Patterns Applied (Addison Wesley, 1998).
[4] "Information Technology Programming Languages C++," International Standard ISO/IEC 14882-1998(E).
[5] Ben-Yehuda, S. C++ Design Patterns course, SELA Labs, http://www.sela.co.il.
Evgeniy Gabrilovich is a Strategic Development Team Leader at Comverse Technology Inc., a developer of multimedia communications processing technology. He holds an M.Sc. in Computer Science from the Technion - Israel Institute of Technology. His interests involve natural language processing, artificial intelligence, and speech processing. He can be contacted at gabr@acm.org.