Click here to Skip to main content
14,739,661 members
Articles » Languages » C / C++ Language » Templates
Article
Posted 22 Nov 2020

Stats

4.1K views
74 downloads
2 bookmarked

Robust C++: Singletons

Rate me:
Please Sign up or sign in to vote.
4.50/5 (2 votes)
25 Nov 2020GPL3
Yet another article on this topic?!
Are singletons ever appropriate? Is there a thread-safe way to create them? How can they be implemented? This article discusses these questions.

Introduction

Singletons need little introduction. A search on CodeProject alone turns up well over 50 articles about them.

So why another article? Well, various issues keep cropping up, and I wanted to weigh in on them. There are also some wrinkles regarding how the Robust Services Core (RSC) implements singletons, and I wanted to document them in an article. I'll try to keep this pithy.

Objections to Singletons

The Wikipedia entry on singletons links to several articles that consider them evil, and those articles make some points that you should consider when deciding whether to use a singleton.

  • A singleton is like a global variable.

If access to the singleton's instance pointer is encapsulated, along with the singleton's data, what's the problem?

  • A class shouldn't care if it's a singleton. This violates the single responsibility principle, so a factory should create the singleton instead.

If the class doesn't care after it's been created, then the factory is just boilerplate, put there to satisfy a religious edict. Almost every rule has exceptions that allow it to be broken.

  • A singleton creates tight coupling between classes. Clients know it's a singleton, so you can't easily replace it with something polymorphic, for example.

If only one instance of a class is needed, this argument makes no sense.

There may also be times when using a singleton makes things easier, even though you know that multiple instances will eventually have to be supported. There is nothing wrong with this—provided that you have a plan for how to evolve your software. Good systems grow organically; trying to build everything right at the outset is usually leads to failure.

  • A singleton's state persists, which can make testing difficult.

Provide a way to reset or recreate the singleton.

  • A singleton wastes memory or resources when no one is using it.

Use reference counting to destroy the singleton or release its resources. If this causes undue overhead, keep it or its resources around. Unless you have a lot of low usage singletons, it'll be much easier to just  leave them allocated.

  • Some languages don't readily support singletons.

This says something about those languages but nothing about the validity of singletons.

  • Subclassing a singleton is almost impossible.

Then it shouldn't be a singleton.1

  • In a multi-threaded environment, you might end up with multiple instances of a singleton.

Various articles discuss the need to lock when accessing a singleton. This adds a lot of overhead, which they try to minimize by only acquiring the lock when the singleton doesn't already exist. But even this is perilous, because simply accessing the singleton's instance pointer opens the door to race conditions. Maybe this can be solved by using an atomic variable, but it's not as easy as you would think….

Enough artificial complexity already! There's only going to be one instance of the singleton, so create it during system initialization, when only one thread ought to be running. If there are times when the singleton truly needs to be destroyed and recreated, assign this responsibility to a specific thread.

All of this has given us some guidelines for using singletons:

  • Be sure that a single instance of the class will always be enough. If it won't be, have a plan for evolving your software to support multiple instances.
  • Create singletons during system initialization.
  • Consider providing a function that returns the singleton to its initial state to simplify testing.
  • If destroying and recreating the singleton is a requirement, make a specific thread responsible for this.

Now we can look at how to implement singletons.

A Singleton Template

One benefit of templates is that they avoid the need to duplicate code. This confines changes to one place when implementing an enhancement or bug fix. The management of singletons falls into this category.

First, the introductory comments to the singleton template:

//  Class template for singletons.  A singleton for MyClass is created and/or
//  accessed by
//    auto c = Singleton< MyClass >::Instance();
//  This has the side effect of creating the singleton if it doesn't yet exist.
//
//  MyClass must define its constructor or destructor as private.  That way, it
//  can only be created via its singleton template.  It must make this template
//  a friend class to enable access to the private constructor and destructor:
//
//    class MyClass : public Base  // actually a *subclass* of Base: see below
//    {
//       friend class Singleton< MyClass >;
//    public:
//       // interface for clients
//    private:
//       MyClass();   // cannot have any arguments
//       ~MyClass();
//    };
//
//  The type of memory that a singleton wishes to use determines it ultimate
//  base class:
//    o MemTemporary:  Temporary
//    o MemDynamic:    Dynamic
//    o MemPersistent: Persistent
//    o MemProtected:  Protected
//    o MemPermanent:  Permanent
//    o MemImmutable:  Immutable
//
//  Singletons should be created during system initialization and restarts.
//
template< class T > class Singleton
{
   // details below
};

RSC supports restarts, which are a way to partially reinitialize a system. To that end, it provides the memory types mentioned in the above comment. Each memory type is characterized by what types of restarts it survives and whether it is write-protected when the system is in service. We will now see the wrinkle that this introduces when managing singletons.

Here is the function that creates or accesses a singleton:

//  Creates the singleton if necessary and returns a pointer to it.
//  An exception occurs if allocation fails, since most singletons
//  are created during system initialization.
//
static T* Instance()
{
   //  The TraceBuffer singleton is created during initialization.
   //  If initialization is being traced when this code is entered
   //  for that purpose, invoking Debug::ft will create TraceBuffer,
   //  so it will have magically appeared when the original call to
   //  this function resumes execution.  We must therefore recheck
   //  for the singleton.
   //
   if(Instance_ != nullptr) return Instance_;
   Debug::ft(Singleton_Instance());
   if(Instance_ != nullptr) return Instance_;
   Instance_ = new T;
   auto reg = Singletons::Instance();
   auto type = Instance_->MemType();
   reg->BindInstance((const Base**) &Instance_, type);
   return Instance_;
}

There are a few things to note here:

  • This code is not thread safe. RSC primarily uses cooperative scheduling so that it rarely needs to protect critical regions at a granular level. If you're using this template outside RSC, you need to consider thread safety if you're not following the advice to create a singleton during initialization or from a specific thread.
  • RSC provides a function trace tool that uses a singleton trace buffer. If the tool is enabled, invoking it (through Debug::ft) will, in itself, create that singleton, so the code must check to see if this occurred.
  • A restart frees memory by freeing the heap that provides the memory. Any singleton on such a heap disappears, so its instance pointer must be nullified. Rather than force every singleton to deal with this, the template adds each one to the global Singletons registry. The registry's primary responsibility is to nullify the instance pointers of each singleton that a restart will blow away.

Earlier, we noted that some singletons may want to support deletion:

//  Deletes the singleton if it exists.  In some cases, this may be
//  invoked because the singleton is corrupt, with the intention of
//  recreating it.  This will fail, however, if the call to delete
//  traps and our static pointer is not cleared.  Even worse, this
//  would leave a partially destructed object as the singleton.  It
//  is therefore necessary to nullify the static pointer *before*
//  calling delete, so that a new singleton can be created even if
//  a trap occurs during deletion.
//
static void Destroy()
{
   Debug::ft(Singleton_Destroy());
   if(Instance_ == nullptr) return;
   auto singleton = Instance_;
   auto reg = Singletons::Instance();
   reg->UnbindInstance((const Base**) &Instance_);
   Instance_ = nullptr;
   delete singleton;
}

Again, a couple of things:

  • The singleton must be removed from the Singletons registry.
  • Destroy is not invoked to delete a singleton during a restart. A restart just frees a heap without invoking the destructors of objects on that heap. This makes a restart much faster than it would otherwise be. If an object owns resources that need to be freed during a restart, it must provide a Shutdown function to release them.

Next, a trivial function that can be very useful when resolving initialization order problems:

//  Returns a pointer to the current singleton instance but does not
//  create it.  This allows the premature creation of a singleton to
//  be avoided during system initialization and restarts.
//
static T* Extant() { return Instance_; }

Now for the template's private implementation details:

//  Creates the singleton.
//
Singleton() { Instance(); }

//  Deletes the singleton.
//
~Singleton() { Destroy(); }

//  Declaring an fn_name at file scope in a template header causes an
//  avalanche of link errors for multiply defined symbols.  Returning
//  an fn_name from an inline function limits the string constant to a
//  single occurrence, no matter how many template instances exist.
//
inline static fn_name
   Singleton_Instance() { return "Singleton.Instance"; }
inline static fn_name
   Singleton_Destroy()  { return "Singleton.Destroy"; }

//  Pointer to the singleton instance.
//
static T* Instance_;

Finally, the singleton's instance pointer—a static member—needs to be initialized. Note that this must be done after (outside of) the class template:

//  Initialization of the singleton instance.
//
template< class T > T* NodeBase::Singleton< T >::Instance_ = nullptr;

Although the Singletons registry is also a singleton, it can't use the template because the Instance function would try to add the registry to itself. It therefore clones the code that it needs from the template.

The Static Singleton

User megaadam commented that the following implementation, which is discussed on Stack Overflow, is thread safe as of C++11:

Singleton& Singleton::Instance()
{
   static Singleton s;
   return s;
}

There are a few places where RSC uses this to resolve initialization order problems. However, it doesn't support RSC's memory types, which the template does. It can only create the singleton in regular, pre-allocated memory, or on the default heap if modified to use new. However, it is a useful technique to be aware of.

Usage

RSC uses singletons in various situations.

Registries. RSC has many registries, each of which tracks all the objects that derive from a common base class. Each registry is a singleton that provides access to its registrants using an identifier that distinguishes the various polymorphs. A template also implements much of a registry's behavior.

Flyweights. Many registries are populated by flyweights. Having more than one instance of a flyweight is very wasteful, so each is a singleton. For example, RSC's state machine framework defines the classes Service, State, and EventHandler, all of whose leaf classes are flyweights that are placed in a registry. There is a global registry for services, and each service has registries for its states and event handlers. This supports a table-driven approach in which a service identifier, state identifier, and event identifier combine to look up and invoke the correct event handler.

Memory. Each heap and object pool is implemented by a singleton.

Threads. RSC has many singleton threads. Someday, a few of these will no longer be singletons, but for now they still are:

  • RootThread wraps the thread created for main.
  • InitThread initializes the system and schedules threads once it is in service.
  • CoutThread front-ends all writes to cout.
  • CinThread front-ends all reads from cin.
  • LogThread spools logs to the console and a log file.
  • FileThread front-ends a file that is written to by more than one thread.
  • CliThread parses and executes commands entered through the CLI.
  • StatisticsThread generates periodic statistics reports.
  • ObjectPoolAudit returns a leaked memory block to its object pool.
  • TimerThread implements lightweight timers for state machines.

Notes

1 One benefit of writing an article is that it makes you revisit old code. The comments for my singleton template noted that a singleton's constructor and destructor should be private—or protected if subclasses had to be supported. This thoughtless comment has now been revised.

History

  • 25th November, 2020: Updated to mention thread safety and static singletons
  • 22nd November, 2020: Initial version

License

This article, along with any associated source code and files, is licensed under The GNU General Public License (GPLv3)

Share

About the Author

Greg Utas
Architect
Canada Canada
Author of Robust Services Core (GitHub) and Robust Communications Software (Wiley, 2005). Formerly Chief Software Architect of the core network servers that handle the calls in AT&T's wireless network.

Comments and Discussions

 
SuggestionProposal for an implementation as a compromise between flexibility and guideline Pin
Bernd Schroeder1-Dec-20 2:11
MemberBernd Schroeder1-Dec-20 2:11 
GeneralRe: Proposal for an implementation as a compromise between flexibility and guideline Pin
Greg Utas1-Dec-20 2:25
mvaGreg Utas1-Dec-20 2:25 
GeneralRe: Proposal for an implementation as a compromise between flexibility and guideline Pin
Bernd Schroeder1-Dec-20 2:46
MemberBernd Schroeder1-Dec-20 2:46 
GeneralRe: Proposal for an implementation as a compromise between flexibility and guideline Pin
Greg Utas1-Dec-20 3:09
mvaGreg Utas1-Dec-20 3:09 
QuestionC++11 Pin
megaadam24-Nov-20 0:33
professionalmegaadam24-Nov-20 0:33 
AnswerRe: C++11 Pin
Greg Utas24-Nov-20 2:53
mvaGreg Utas24-Nov-20 2:53 
GeneralRe: C++11 Pin
megaadam26-Nov-20 6:03
professionalmegaadam26-Nov-20 6:03 
GeneralRe: C++11 Pin
Greg Utas26-Nov-20 6:19
mvaGreg Utas26-Nov-20 6:19 
GeneralMy vote of 5 Pin
Andreas Saurwein23-Nov-20 1:16
MemberAndreas Saurwein23-Nov-20 1:16 
GeneralRe: My vote of 5 Pin
Greg Utas23-Nov-20 1:49
mvaGreg Utas23-Nov-20 1:49 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.