Destruction-Managed Singleton: A Compound Pattern for Reliable Deallocation of Singletons
- By Evgeniy Gabrilovich
- February 24, 2000
Singleton
1 is a creational pattern with well-defined semantics that ensures the instance is always created prior to use. This effectively solves the problem of initialization order when a number of interrelated objects are involved. But the pattern's destruction semantics is inadequate for several singletons with complex dependencies among them. A
Destruction-Managed Singleton complements this pattern by imposing a sound order of object destruction. Destruction-Managed Singleton is an instance of the Object Lifetime Manager pattern,
2 which "governs the entire lifetime of objects, from creating them prior to their first use to ensuring they are destroyed properly at program termination."
INTENT Ensure the destruction of interdependent singletons in the correct order, and guarantee that there are no attempts to use previously deallocated objects.
MOTIVATION For example, suppose there is a global Logger object, and methods of other global entities use it for recording various status messages. Suppose further that the destructors of these entities must notify the Logger about system resources they release. Obviously, the Logger should be the last one to be destroyed. If the language rules destroy the Logger first, other objects might unknowingly attempt to use it, leading to unpredictable (and probably disastrous) consequences.
This problem can be solved by using a dedicated Destruction Manager to control the order of singleton destruction. Whenever a singleton is created, its constructor notifies the Destruction Manager of when it should be destroyed (relative to other singletons). The constructor creates a destructor object, which contains a pointer to the singleton and its (user-assigned) destruction phase.* The destructor is automatically registered with the Destruction Manager, which assumes responsibility for the corresponding singleton from then on. At the end of main(), the programmer should invoke a dedicated function of the Destruction Manager (destroy_objects()). The latter sorts all the destructors registered with it in the decreasing order of their phases, and then destroys the singletons in this order. The destruction per se is performed by calling a function destroy() of the destructor, which in turn invokes the function destroy_instance() of the singleton (accessible to the destructor due to its friend relationship with the latter).
From the structural point of view, Destruction-Managed Singleton is a compound pattern. It utilizes the reciprocity between Singleton and Destruction Manager, using the notion of registration.† Composite design patterns (or compound patterns)‡3 are such that their basic building blocks are patterns themselves, rather than objects. The key notion in this definition is the synergy between the constituent patterns, which accounts for the ability of individual patterns to work together to become a more useful pattern.
Since all the clients of a Singleton class share its lone instance, the design should impose a controlled protocol regarding the deletion of this object. Moreover, special care must be taken of the destruction order in the presence of several singleton objects that depend on each other.4 The Destruction Manager addresses exactly this situation: objects register with it for subsequent destruction, and each object is only destroyed when it is no longer needed. Observe that the Destruction Manager is not specific about the creation of objects, it only cares about their destruction. In fact, the Destruction Manager and the so-called creational patterns1 lie on the opposite sides of the object lifetime management spectrum. Incidentally, since a single Destruction Manager is usually sufficient, and it should be globally available for other objects to register, the Destruction Manager itself may be implemented as a Singleton.
APPLICABILITY If some global object is a client of another one, the latter may not be destroyed until the former terminates. Otherwise, if the former inadvertently invokes a function of the latter after it has ceased to exist, the aftermath may be rather gloomy. The C++ Standard5 prescribes the order of initialization and only for objects defined in the same translation unit. In all other cases, use Destruction-Managed Singleton for safe deallocation of interdependent global objects.
STRUCTURE Figure 1 represents the class diagram of the Destruction-Managed Singleton.
Figure 1. Class diagram for Destruction-Managed Singleton.
PARTICIPANTS
DestructionManager
- Responsible for destroying registered singletons in user-defined phases.
- Provides function instance() to access the unique instance (singleton interface).
- Singleton destructors register themselves using the register_destructor() function.
- The application client invokes function destroy_objects() when graceful shutdown is required.
- The destructor of the Destruction Manager (~DestructionManager()) deallocates all the destructor objects registered with it. As explained, these auxiliary objects are dynamically created by singleton constructors, and have to be disposed of properly to prevent memory leak.
DestructionPhase
- Encapsulates the notion of a destruction phase.
- Destruction phases can be compared using the boolean operator>().
Destructor
- An abstract base class which represents objects to be destroyed in a particular phase.
- The constructor receives a phase parameter and registers itself with the Destruction Manager so that its function destroy() will be invoked in this phase.
- Pure virtual function destroy() is overridden in derived classes. Its invocation destroys the object represented by the destructor.
- Destructors are comparable to one another, based on the values of their phases, via the boolean operator>().
TDestructor
- A parameterized (template) class, whose instances enclose pointers to actual objects to be destroyed.
- The constructor receives a pointer to an object and a phase in which the object should be destroyed, and registers with the Destruction Manager due to the implementation of the constructor of the base class.
- Function destroy() literally destroys the underlying object by calling its function destroy_instance() (which is assumed to be defined in all the classes instantiating the template).
Singleton
- In this design, singleton represents a generic singleton object whose destruction should be controlled.
- instance() is a vanilla access function.
- The constructor creates a new destructor object of type TDestructor to represent this singleton. This destructor keeps a pointer to the singleton and the designator of its destruction phase.
- Function destroy_instance() destroys the singleton.
- Finally, method() stands for all the other member functions of the singleton that define its specific behavior. For example, in a Logger class such as was mentioned, the function void Logger::log(string message), which provides a message logging service for its clients.
Figure 2. Interaction diagram for Destruction-Managed Singleton.
COLLABORATIONS The sequence diagram in Figure 2 depicts sample collaborations between the participants of the Destruction-Managed Singleton:
- The application requests access to the singleton instance to invoke its method().
- No instance has yet been created and the static function instance() creates one by calling the singleton constructor.
- The Singleton constructor creates a destructor object of type TDestructor to keep the singleton pointer and its destruction phase.
- The destructor attempts to obtain a reference to the Destruction Manager.
- No Destruction Manager exists, so the static function instance() creates one.
- The destructor registers itself with the Destruction Manager.
- The application invokes the method() member function of the singleton.
- Toward the end of the program, the application is about to perform a clean shutdown. To this end, it first obtains a reference to the Destruction Manager.
- The application calls function destroy_objects() of the Destruction Manager.
- The Destruction Manager sorts the destructors registered with it in decreasing order of their phases, using the operator>().
- Upon sorting, the Destruction Manager invokes function destroy() of each destructor.
- The destructor forwards the destruction request to the singleton it represents, by calling its function destroy_instance().
- The static function destroy_instance() deletes the singleton.
- At the end of the program, the destructor of the Destruction Manager is automatically invoked.
- The destructor of the Destruction Manager deallocates all the destructor objects registered with it.
IMPLEMENTATION We use an implementation of Singleton
6 based on the
auto_ptr class template-the C++ Standard Library
5 definition of smart pointer. This way, if a singleton is not controlled by the Destruction Manager (e.g., the Destruction Manager is also realized as a singleton, but does not destroy itself), the auto-pointer mechanism destroys the singleton object at program end.
§
The 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-pointer to the actual object, which serves as a proxy for the latter. The object instance owned by the auto_ptr is thus detached from the access function wrapper (instance()) that ensures its singleton properties. It is this feature that allows the singleton object to be destroyed either by the auto_ptr itself or by the Destruction Manager. In the latter case, function auto_ptr::release() is used to retrieve the singleton pointer; it revokes the ownership of the auto_ptr over the singleton and thus prevents its repeated deletion.#
The Singleton interface has two public access functions-const and non-const-which yield the singleton object by dereferencing the auto_ptr. For additional code safety, if the constant object will do the job, the access function const_instance() should be used. Both functions return a reference to the actual object (and not to the auto_ptr) to conceal the implementation details from clients.
Implementation and Sample Code for the Destruction-Managed Singleton This section exemplifies the gist of the solution. The complete example code is available at the C++ Report Web site at http://www.adtmag.com/joop/index.asp.
Listing 1 shows the hierarchy of classes for destructor objects. Destructor is an abstract class whose instances can be sorted according to their destruction phases. Class template TDestructor<> is derived from Destructor, and contains a pointer to the singleton object it is responsible for. The template parameter is instantiated to actual singleton classes that are assumed to define the function destroy_instance(), which allows deletion of the object inside the auto_ptr (that's why it is necessary to detach the object from its wrapper).
The Destruction Manager is outlined in Listing 2 (some obvious details due to the Destruction Manager's being a singleton have been omitted for the sake of brevity). Singletons register their destructors with the Destruction Manager via the function register_destructor(). The Destruction Manager is responsible for the memory occupied by (dynamically allocated) destructor objects; therefore, it has to delete them before it terminates.
Function destroy_objects() sorts the destructors in decreasing order of their phases, then consecutively destroys the singleton objects they manage. It is this function that should be manually invoked at the end of main() to ensure proper destruction order of the program singletons. Class template greater_ptr<> is an auxiliary predicate for comparing objects given their pointers.
It is the responsibility of the programmer to invoke function destroy_objects() on exit from main(). Observe that the destructor of DestructionManager does not call this function because there is no control of when the destructor is invoked. For example, it may be invoked after the application shutdown process has started, in which case it is too late to call destroy_objects(), since some of the singletons may have already been destroyed.
Listing 3 shows a sample resource definition-a message logging class Logger (again, insignificant details have been omitted). The Logger here corresponds to the generic Singleton class of the sections "Structure," "Participants," and "Collaborations." The Logger constructor creates a new TDestructor object, which contains all the information necessary to destroy the Logger object at the right time. The constructor of TDestructor registers it with the Destruction Manager; the latter ultimately deletes the logger, and then deallocates the destructor object itself.
Function destroy_instance() releases the singleton object from the auto_ptr, then destroys it. Since the auto_ptr no longer owns the object, it will not attempt to delete it when the program terminates. Class TDestructor is defined as a friend of Logger, so that its instances may access the function destroy_instance().
Note the implementation of the instance() function: it does not immediately return the dereferenced auto_ptr (in contrast to Listing 1), but first checks if it still points to a valid object. This test may fail in either of the following two cases:
- Memory allocation has failed|| in function get_instance().
- The singleton object has already been destroyed.
The former case is extremely rare. The latter may occur if incorrect destruction phases have been assigned in the program, and some singleton is deleted prior to its last use. In such a case, when the Destruction Manager destroys the singleton via function
release() of the
auto_ptr, the data member pointer of the latter is cleared (see previous footnote
#), and this causes the validity test to fail. Although this test incurs a slight runtime penalty on each singleton access, it should be used at least during the debugging stage to verify the destruction policy. The idea of checking singletons for prior deletion comes from Andrei Alexandrescu.
8
A word of caution: If a singleton's destructor invokes a member function of another singleton that has already been destroyed, an std::logic_error exception will be thrown. It is dangerous for this exception to leave the destructor, for if the latter was invoked during stack unwinding due to an exception thrown earlier, function terminate() will be immediately called, aborting the application.5 This situation is discussed in More Effective C++.9
The complete example code (available on the C++ Report Website-http://www.creport.com) also defines class Resource, whose destructor uses function Logger::log() to record error messages (if any).
At last, Listing 4 presents the function main(). The function DestructionManager::instance.destroy_objects() is invoked by the destructor of a utility class DestroyObjects, thus relieving the programmer from having to call it explicitly at the end of main(). This approach facilitates multiple exit points from main(), so it is not necessary to copy the cleanup code over and over again. More important, it also works in case of exceptions propagating through main(), since destructors of local objects are automatically invoked during stack unwinding. If destroyer is the first local variable defined in main(), its destructor would be called last;5 thus, the Destruction Manager would be invoked immediately before leaving the function.
In the example code available at the C++ Report Website, class DestroyObjects is defined in file "dmanager.h", following the definition of class DestructionManager. This is more convenient to clients, as the Destruction Manager comes complete with this auxiliary class.
CONSEQUENCES Destruction-Managed Singleton is a compound pattern for controlling the destruction order of singleton objects. It achieves this aim due to the cooperation of the following individual patterns and techniques:
- The Singleton pattern resolves the initialization order of interdependent global objects and ensures the program-wide uniqueness of the resource it manages.
- The Destruction Manager is a complementary pattern for managing the other end of objects' life span, namely, destruction. Therefore, it may be considered a destructional pattern, as opposed to creational patterns1 like Abstract Factory, Factory Method, or Singleton itself.
- The Proxy pattern (realized through the auto_ptr implementation of the concept of smart pointer) allows the program to detach the Singleton object from its wrapper and thus facilitates its destruction by the Destruction Manager (bypassing the regular auto_ptr mechanism).
- The Registration technique enables singletons to sign up with the Destruction Manager for subsequent destruction at an appropriate time.
The Destruction-Managed Singleton handles the entire life span of a singleton, extending the behavior of the original pattern.
1 This compound pattern guarantees the object is created prior to use and exists as long as it is needed. In addition to enforcing the destruction policy, the Destruction-Managed Singleton constantly verifies its validity and throws an exception whenever a sound destruction order is breached.
DISCUSSION This section examines several incidental issues, including the tradeoffs incorporated in the Destruction-Managed Singleton.
Managing Nonsingleton Objects When a program uses global objects which are not singletons, and especially if there are singletons that depend on nonsingletons, the more comprehensive Object Lifetime Manager2 should be used. This pattern controls creation and destruction of objects that are not necessarily singletons, and is available as a built-in part of ACE.10
Alternative Destruction Policies Instead of using an express notion of destruction phases, an alternative approach could require each singleton to explicitly specify on which other objects it depends. The Destruction Manager would then perform a topological sort of the dependencies graph, deducing the phases of destruction automatically. This would require the constructor of each singleton to notify the Destruction Manager of all the other singletons it depends on. In a simple solution where the constructor registers pointers to all the other singletons it might use, all those would be created even if some of them are not needed in a particular program execution. A more elaborate solution would identify singletons by their (unique) string names rather than pointers, but this seems to be overkill.
Thread-Safety Among the issues this article does not address is thread-safety. Probably every existing singleton implementation uses variables defined at global scope. In a multitasking environment this may constitute a problem, should a singleton have to serve multiple threads. The Double-Checked Locking pattern11 presents a good solution to this problem, minimizing the amount of coordination necessary for preserving the consistency of critical sections. Vlissides notes that in multithreaded applications it is well advised to use an access function instance() that returns a pointer to the singleton rather than a reference. This is because "some C++ compilers generate internal data structures that cannot be protected by locks."4
Genericity Finally, it should be mentioned that ACE10 provides a reusable singleton adapter, ACE_Singleton, which accepts a user-defined class as a parameter and makes it a Singleton. The idea is based on the concept of separation of responsibilities between the two classes: the user class (also known as the adapted class) and the adapter class. The latter is solely responsible for the "Singleton-ness" of the former, which can concentrate on the business modeling aspects per se. The Double-Checked Locking technique10 is already incorporated in this adapter, which contains an additional parameter to facilitate different locking policies. If necessary, the ACE_Singleton template can be easily integrated into the Destruction-Managed Singleton.
Acknowledgments Destruction-Managed Singleton was inspired by an example code from S. Ben-Yahuda's C++ Design Patterns course,12 where an initialization manager working in phases was used to resolve the mutual initialization order of global variables (without singletons, but using two-stage object construction).
Special thanks are due to Brad Appleton, Patterns++ Section Editor, for many enlightening discussions and for his guidance during the preparation of this article. Thanks to Avner Ben and Vitaly Surazhsky for their constructive comments and suggestions.
References
- Gamma, E. et al. Design Patterns: Elements of Reusable Software Architecture, Addison-Wesley, Reading, MA, 1995.
- Levine, D. L., C. D. Gill, and D. C. Schmidt. "Object Lifetime Manager-A Complementary Pattern for Controlling Object Creation and Destruction," C++ Report, 12(1): 31-40, 44, Jan. 2000.
- Vlissides, J. "Composite Design Patterns," C++ Report, 10(6): 45-47, Jun. 1998.
- Vlissides, J. Pattern Hatching: Design Patterns Applied, Addison-Wesley, Reading, MA, 1998.
- "Information Technology-Programming Languages-C++," International Standard ISO/IEC 14882-1998(E).
- Gabrilovich, E. "Controlling the Destruction Order of Singleton Objects," C/C++ Users Journal, Oct. 1999.
- Meyers, S. Effective C++, 2nd ed., Addison-Wesley, Reading, MA, 1998.
- Alexandrescu, A. Private correspondence, 1999.
- Meyers, S. More Effective C++, Addison-Wesley, Reading, MA, 1996.
- Schmidt, D. C. "ACE: An Object-Oriented Framework for Developing Distributed Applications," in Proceedings of the 6th USENIX C++ Technical Conference, Cambridge, MA, USENIX Association, Apr. 1994.
- Schmidt, D. C. and T. Harrison. "Double-Checked Locking-An Optimization Pattern for Efficiently Initializing and Accessing Thread-Safe Objects," in Pattern Languages of Program Design, M. Buschmann and D. Riehle, Eds., Addison-Wesley, Reading, MA, 1997.
- Ben-Yehuda, S. "C++ Design Patterns" course, SELA Labs. (http://www.sela.co.il).
FOOTNOTES
* The smaller the phase, the later the singleton should be destroyed. We assume there are no circular dependencies between singletons, so it is possible to assign each one an appropriate destruction phase.
† Though not a pattern in its own right, registration of objects with some distinguished entity is a technique frequently used in patterns (e.g., Observer and registration of Prototypes in Abstract Factory, to name but a few).
‡ The name "compound pattern" is used hereafter, to keep with John Vlissides' current usage of the term.
§ In Effective C++,7 Meyers suggests a Singleton implementation where the object instance is defined as static in a dedicated function, which returns a reference to it. Such a definition invokes the singleton destructor prior to program termination, thus preventing possible memory and resource leak. Observe that this approach tightly binds the singleton object with the enclosing function.
# According to the approved C++ Standard5 (and as opposed to its previous draft editions), the release() function must set the auto_ptr data member pointer to NULL. This prevents repeated deletion when the auto_ptr checks the ownership over the pointed object in its destructor.
|| Unless the operator new throws an std::bad_alloc exception on failure.