The Singleton in C++ - A force for good?

The Singleton in C++ - A force for good?

By Alexander Nasonov

Overload, 14(76):, December 2006


Alexander Nasonov addresses some problems that arise when using Singleton in C++.

According to [ Alexandrescu ], the Singleton pattern has a very simple description but can have complex implementations. Indeed, [ GoF ] states that this pattern limits an object to one instance in the entire application, then they give an example. It doesn't seem very complex but it isn't that simple (see Listing 1).

 // Declaration
 class Singleton {
 public:
   static Singleton* Instance();
 protected:
   Singleton();
 private:
   static Singleton* _instance;
 };

 // Definition
 Singleton* Singleton::_instance = 0;
 Singleton* Singleton::Instance() {
   if(_instance == 0) {
       _instance = new Singleton();
   }
   return _instance;
 }
  
Listing 1

Actually, the _instance doesn't always point to one object. Before a first call to Singleton::Instance , there is no Singleton object. And this they call Singleton ???

You may argue that this mechanism is completely transparent to a user but that's not quite correct.

Systems with tight time-constraints would suffer from the fact that calling this accessor function may take as long as the longest execution path in Singleton constructor. This time should be added to every function that calls Singleton::Instance even though this long path is executed only once 1 .

There is another visible effect in multi-threaded programs caused by unprotected concurrent access to Singleton::Instance . Refer to [ DCLocking ] for more details.

What's more, the object is never deleted!

Why did the authors select this approach if they could just wrap a global variable to ensure one instance of a Singleton, as shown in Listing 2?

 // Declaration
 class Singleton : private boost::noncopyable {

 public:
   static Singleton instance;

 private:
   Singleton();
   ~Singleton();
 };

 // Definition
 Singleton Singleton::instance;
  
Listing 2

The authors should have had a very strong reason to prefer the first solution. Unfortunately, the book doesn't explain it well.

Modern C++ Design has a special chapter about singletons. It answers what's wrong with the second code snippet. The order of initialisation of objects from different translation units is unspecified. It's possible to access an uninitialised object during the initialisation of another object as demonstrated by this code:

     // Somewhere in other TU:
         int undefinedBehavior = Singleton::instance.getAccess();
    

The [ GoF ] version fixes this problem because the Singleton instance is automatically created when Singleton::Instance is called for the first time:

     // Somewhere in other TU:
         int goodBehavior
            = Singleton::Instance()->getAccess();
    

The question is, does this really make it better?

First of all, it doesn't completely define an order of initialisation. Although dependencies at initialisation time are tracked correctly, they define only a partial ordering. It's worthwhile to note that these dependencies are built at run-time, that is, they may vary from run to run. Often these dependencies are not clear to developers (they tried to manage them automatically, after all!).

You may wonder why track the dependencies if all singletons are initialised correctly. Don't forget that the program should stop its execution correctly as well. Singletons not only depend on other singletons at initialisation time but also when they are destroyed.

One fundamental principle of C++ is "first initialised, last destroyed". It helps to maintain a list of objects that can be used in a destructor of some global object. That list contains only global objects constructed earlier than this object.

The [ GoF ] solution doesn't rely on this principle of C++ because objects are created on the heap. They don't explain how to destroy these objects, though. You can read Modern C++ Design for a good explanation of managing singleton lifetimes.

All these problems keep me away from using this code unless a framework as good as the boost libraries appears on my radar screen. Until then I stick to global variables with uniqueness guarantees (if affordable) and rely on compilers that allow me to specify an order for non-local objects initialisation.

I found the following principle very useful:

  • Minimise global variables to a bare minimum
  • Minimise dynamic initialisation of global variables

Static initialisation doesn't have a dependency problem because all objects are initialised with constant expressions. Compare:

       pthread_mutex_t g_giant_mutex
              = PTHREAD_MUTEX_INITIALIZER;
    

with

       boost::mutex g_giant_mutex;
           // User-defined constructor
    

The former guarantees initialisation before dynamic initialisation takes place. The latter does not. Please note that private constructors required to ensure uniqueness enforce dynamic initialisation.

Protect yourself from access to uninitialised objects

Memory zero-initialised at the static initialisation phase may be accessed even though an object is not yet constructed. Sometimes it crashes the program but it can also yield surprising results.

For example:

       // TU 1
           money dollar(1.0, "USD");
    
       // TU 2
           double valueOfDollar = dollar.getValue();
    

If initialisation starts with valueOfDollar , dollar.getValue() may return 0.0 instead of 1.0 .

To protect yourself from this kind of access, you can use a boolean global variable constructed during the static initialisation phase. This variable is true only if an object it controls is alive. See Listing 3.

 // Declaration
 class Singleton : private boost::noncopyable {
 public:
   static Singleton* instance();
 private:
   Singleton();
   ~Singleton();
   static bool g_initialised;
   static Singleton g_instance;
 };

 // Definition
 bool Singleton::g_initialised;
   // static  initialisation
 Singleton Singleton::g_instance;
   // dynamic initialisation
 Singleton::Singleton()
 {
   g_initialised = true;
 }

 Singleton::~Singleton()
 {
   g_initialised = false;
 }

 Singleton* Singleton::instance()
 {
   return g_initialised ? &g_instance : 0;
 }
  
Listing 3

Control dependencies in constructors of global variables

My preferred method is not to use any global variables directly. It's best demonstrated by example:

     Singleton Singleton::g_instance(Logger::instance(),
                                         Factory::instance());
    

All Singleton's dependencies are visible at the point of definition. They are easily accessible inside the Singleton constructor as actual arguments. You don't need to access them through the instance functions.

Don't introduce more dependencies in a destructor

In general, you should use only dependencies defined at construction time. This ensures that those objects are not destroyed (remember the first constructed, last destroyed rule).

However, there is one exception; some singletons such as Logger should be accessible from all other singletons. You can implement a technique similar to iostream objects initialisation or use non-virtual functions that don't access member variables if Logger is already destroyed 2 (see Listing 4).

 void Logger::log(char const* message)
 {
     if(!g_initialised)
         std::cerr << "[Logger is destroyed] "
            << message << '\n';
     else
         // Put your cool logger code here
 }
  
Listing 4

Build your program in more then one way

Nowadays, people rarely link statically. However, many loaders manage dependencies among shared libraries automatically and hide potential problems. It's harder to setup a static build but it's usually worth it.

Conclusion

I recently came across an interesting blog entry of Mark Dominus. He says: Patterns are signs of weakness in programming languages. I don't completely agree with this statement because even if you put all patterns in a language you'll soon discover other patterns. But I agree with Mark that the Singleton pattern is a sign of weakness of the C++ language.

Whilst there are many ways to implement a singleton, the method provided in Listing 5 fulfils the criteria presented in this article.

 // Singleton.hpp
 #ifndef FILE_Singleton_hpp_INCLUDED
 #define FILE_Singleton_hpp_INCLUDED
 #include <boost/noncopyable.hpp>
 class Singleton : private boost::noncopyable
 {
   public:
     static Singleton* instance();
     void print(char const* str);
   private:
     Singleton();
     ~Singleton();
     static bool g_initialised;
     // static  initialisation
     static Singleton g_instance;
     // dynamic initialisation
     char m_prefix[32]; // to crash the program     };
 #endif // FILE_Singleton_hpp_INCLUDED

 // Singleton.cpp
 #include "Singleton.hpp"
 #include <ostream>
 #include <iostream>
 #include <cstring>
 bool Singleton::g_initialised;
    // static  initialisation
 Singleton Singleton::g_instance;
    // dynamic initialisation

 Singleton::Singleton()
 {
     g_initialised = true;
     std::strcpy(m_prefix, ">>> ");
 }

 Singleton::~Singleton()
 {
     g_initialised = false;
 }

 Singleton* Singleton::instance()
 {
     return g_initialised ? &g_instance : 0;
 }

 void Singleton::print(char const* str)
 {
     std::cout << m_prefix << str;
 }

 // main.cpp
 #include "Singleton.hpp"
 struct X
 {
     X() { Singleton::instance()->print("X\n"); }
     ~X() { Singleton::instance()->print("~X\n"); }
 } x;

 int main()
 {
     Singleton* p = Singleton::instance();
     p->print("Hello, World!\n");
 }
  
Listing 5

References

[ Alexandrescu ] Andrei Alexandrescu. Modern C++ Design

[ DCLocking ] Scott Meyers and Andrei Alexandrescu. C++ and the Perils of Double-Checked Locking , Doctor Dobb's Journal, 2004. http://www.aristeia.com/Papers/DDJ_Jul_Aug_2004_revised.pdf

[ Dominus ] Mark Dominus. Design patterns of 1972 . http://www.plover.com/blog/prog/design-patterns.html

[ GoF ] Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software . Addison-Wesley, 1995.

1 . It can be proved that some functions are never the first to call Singleton::Instance and discount them, but this distinction would complicate the matter even further.

2 . Strictly speaking, calling any function of a dead object is bad. In practice, calling non-virtual function is safe if it doesn't access memory pointed to by this pointer.






Your Privacy

By clicking "Accept Non-Essential Cookies" you agree ACCU can store non-essential cookies on your device and disclose information in accordance with our Privacy Policy and Cookie Policy.

Current Setting: Non-Essential Cookies REJECTED


By clicking "Include Third Party Content" you agree ACCU can forward your IP address to third-party sites (such as YouTube) to enhance the information presented on this site, and that third-party sites may store cookies on your device.

Current Setting: Third Party Content EXCLUDED



Settings can be changed at any time from the Cookie Policy page.