The One: A Singleton Discussion

Published May 24, 2002 by Robin Tan, posted by Myopic Rhino
Do you see issues with this article? Let us know.
Advertisement
This article has since been revised and updated
[size="5"][size="2"](Editor's Note: This article contains samples that are currently not available) Abstract The singleton is a recurring pattern used by programmers. The singleton pattern is easy to understand but implementation-wise in C++ it has lots of subtle problems. This article discusses those problems. The reader is assumed to have a basic understanding of the singleton pattern. There are many articles on the subject available on the net. Please read at least one as I assume some knowledge of singletons. Should there be an error, bug me at [email="robin@cyberversion.com"]robin@cyberversion.com[/email] [size="5"]Why singletons? For starters, let's examine why we need singletons. Take a look at this program.
// main.cpp #include using namespace std; extern int x; class C { public: C() { // may not print 3 cout << x << "\n"; } }; C c; int main() { return 0; } // another.cpp int F() { return 3; } int x = F();
Whether the above program prints 3 or 0 becomes dependent on the order in which you link main.cpp and another.cpp. Switch them around and you get different results. Initialization of static objects is well defined with each translation unit (ie. .cpp file), but not across multiple files. This is why we need singletons. [size="5"]Meyer's Singleton This solution was first proposed by Scott Meyer. It often goes in the form below.
class MyClass { MyClass() {} ~MyClass() {} // undefined MyClass( const MyClass & ); MyClass& operator=( MyClass ); public: static MyClass& Get_Instance() { static MyClass instance; return instance; } };
The ctor (constructor), dtor (destructor), copy ctor and assignment operator are defined privately to avoid unauthorized creation. This simple solution works by having a local static variable inside a function. The variable is initialized the first time Get_Instance() is called and deallocated when main exits. It is an elegant solution that fulfils what many programmers need from a singleton. [size="5"]But.. Meyer's singleton is not without problems. Some immediate problems are
  1. By storing an auto object (one that is allocated on the stack), we cannot store derived classes.
  2. The order of destruction is undefined.
No problem, we store a local pointer instead of local instance. This would allow Get_Instance() to return a reference to its parent interface (perhaps an abstract interface).
// class MyChild : public Parent static Parent& MyChild::Get_Instance() { static Parent *p = new MyChild(); return *p; }
Problem 1 is solved, except that the singleton now leaks because the destructor is never called. The obvious solution here then would be to store a local static smart pointer (like std::auto_ptr) instead of the raw pointer. Now that the first problem is taken care of, let's look at the second. Meyer's singleton is initialized when Get_Instance() is first called. We solved the initialization order problem but another problem remains - the order of destruction. A static variable inside a function is registered by at_exit(), and is deallocated when main exits. We have no control over when it gets called or when the destructors of global objects call the singleton - it may be already deallocated! To prove my point, try this wicked sample that is designed break Meyer's singleton. Notice the order of calls produced. After main exits, the singleton is deallocated yet another call to Get_Instance() is made afterwards. For this simple class it probably wouldn't crash, but if your class has data members, it means those members won't be available anymore. This occurs because we are relying on the compiler to automatically register the deallocation using at_exit(). If we store a pointer instance, we could avoid this issue, but then no one would be deallocating it. We either leak, or risk crashing. It is clear now that Meyer's singleton is insufficient for singletons that needs to be called within destructors of global objects. Granted, such singletons are rare, but what would you do if you faced one? Note: In case you are wondering about what situations you might need a singleton that has defined order of construction/destruction, think about a global logging system. [size="5"]Nifty Counter The nifty counter singleton has a defined order of construction and destruction. This solution to the above problem is described in C++ Faqs. (Not the free online version, the book has more info on it) We observe that local variables used in functions have an undefined destruction order. Only pointers can choose their destruction time. In essence, this is how the nifty counter works. Look at the sample before I explain. A nifty counter class is created to hold a pointer to MyClass. The Get_Instance is removed from MyClass into the nifty counter instead. Note at the end of MyClass.h, there is a static instance of the nifty counter. Before you scream at the atrocity of declaring variables in headers, calm down and look at how it works. Every file that includes MyClass.h will have an local instance of the nifty counter. The Nifty counter class stores an reference count of how many instances it was created. Within each translation unit, the order of initialization/destruction is well defined (I said this before). The order of construction/destruction is first in/last out (FILO). Since we have to include MyClass.h before using the singleton, we are unknowingly creating a local instance of the counter in every translation unit. Since this local counter is always destructed last in the translation unit, the order of destruction is well defined (when no one needs to use it anymore). Does this come for free? Well almost. Notice we have to create a local counter per translation unit. This means that starting/shutting down the program will take more time because we have to page those sections into memory. If you can live with that, this singleton should serve you well. [size="5"]Zerob Singleton This is a bonus for those who have the book Modern C++ Design by Andrei Alexandrescu. This is a singleton holder inspired by the book. In the book's implementation of singleton holder, it keeps track of the order of destruction using a list. I did not particularly like that way of implementation, so I created this singleton holder instead. It follows the book's implementation closely. However, for the singleton lifetime, I provide a nifty counter and Meyer's singleton policy instead. You probably have to read the book to understand how to use it. This singleton holder is a part of a free reusable game toolkit (the availability of which will be announced in the near future). I provide it here as I feel it is relevant to this discussion. Take note though, it requires a very compliant C++ compiler. It currently compiles on Gcc 3.0 and Comeau only. Gcc 2.95, VC++ 6, and VS.Net all fail to compile it (VS.Net can compile with some minor modifications) I haven't tried Borland C++, but I suspect it won't compile it either. This is one reason why more compilers need to be compliant with the standard. Go pressure your vendors. [size="5"]More problems We are not done yet. We have only solved the basic lifetime requirements for singletons. There are other problems. [size="3"]Exception safety Exceptions are normally caught in a try/catch block within main. If the ctor of the singleton throws, chances are you have not entered main yet. This imposes a serious design issue for the singleton. You normally have to design the singleton so that the ctor doesn't throw. However, there is a way to throw exceptions from the ctor. Try/catch blocks can be used at function scope (known as function try blocks). You can design an exception trapping mechanism for global objects using function try blocks. There are archived discussions on USENET comp.lang.c++.moderated regarding this. I shall not go furthur because there is enough material to warrant another long discussion. [size="3"]Thread safety I often find programmers jumping into threads without understanding their complexity. Just becausee they know how to create a mutex/critial section, they think they can do threaded applications. If you must, at least test the code on a multi processor machine. The easiest solution would be to protect Get_Instance with a synchronization object (critical section). That would work but that would mean additional overhead when accessing the singleton. Synchronizing locks/unlocks are usually not cheap and if the singleton is called often, performance can be affected big time. A better design would be to let the client decide when to lock when accessing the singleton. Immediately a problem pops up - there is a possibility that multiple threads are trying to create the singleton on their first call to Get_Instance. An ingenious solution is the "Double Checked Locking Pattern". Alas, it doesn't solve the problem thoroughly as one would hope. In the absence of threading support from the current C++ standard library, the only guaranteed solution is to use some kind of platform specific synchronization object. In that case, the DCLP can be used with the sync object to protect the creation code of the singleton. Note: The "zerob singleton" is not thread safe at the moment. Until C++0x comes up with a portable threading library, I will avoid thread issues when building resuable components. [size="5"]Abuse of singletons Implementing a singleton is fraught with dangers. Singletons provide more safety than using global variables but most of the time, they are still used more often than they should be. Practical experience suggests than it is often possible to redesign so the singleton becomes a member variable or local static variable. Indeed, this should be a worthy goal to strive for. Do we really need the graphics renderer to be global? Can it be a member variable of an Application object instead? Does the factory for creating objects need to be a singleton? Wouldn't it be better if we localize all the creation code in a single module? Think about the functionality of the class. Is it necessary for it to be a singleton or a mere convenience? For convenience sake, take note of the problems you are generating by having a singleton. A redesign would almost always benefit in the long run. Always remember to KISS (Keep it simple stupid).
Cancel Save
0 Likes 0 Comments

Comments

Nobody has left a comment. You can be the first!
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!

Discusses some of the problems encountered when using the singleton pattern, and suggests some solutions, including nifty counters and Zerob singletons.

Advertisement
Advertisement