Exploring Singleton Pattern in C++: Ensuring Unique Instances

Exploring Singleton Pattern in C++: Ensuring Unique Instances

In the software development, managing object instances efficiently is crucial, especially when you require only one instance of a particular class throughout your application's lifecycle. This is where the Singleton design pattern comes into play. In this article, we'll delve into the Singleton pattern in C++, understanding its implementation, use cases, and potential pitfalls.

# Understanding the Singleton Pattern

The Singleton pattern is one of the creational design patterns, aiming to ensure that a class has only one instance and providing a global point of access to it. This pattern involves a class with a private constructor, preventing external instantiation, and a static method to access the single instance.

# Implementation in C++

/*******************************************************************************
*
* Program: Singleton Design Pattern
* 
* Description: Example implementation of the Singleton Design Pattern in C++.
* This implementation is thread-safe and uses lazy instantiation. For more 
* information Singletons see: https://en.wikipedia.org/wiki/Singleton_pattern.
*
*******************************************************************************/
#include <iostream>

using namespace std;

// The Singleton class
class Singleton
{

// We make the constructor a protected member so that Singleton objects cannot
// be constructed from outside the class, to help enforce that only one 
// Singleton object should ever be instantiated.  We use the default specifier 
// to have the compiler-produced default constructor used for our class.
protected:
  Singleton() = default;
  
public:
  
  // We'll give our Singleton object a member variable to help test it out
  int data;
  
  // The static member function get_instance will be responsible for creating 
  // and allowing access to the one Singleton object instance.  Being a static 
  // member function the function is really connected to the Singleton class 
  // rather than any Singleton object instance, so we don't need a Singleton 
  // object instance to exist in order to use it!  The function uses a local 
  // static variable to create a Singleton object instance.  When the function 
  // is first called the Singleton object instance will be created, but 
  // because it's a STATIC local variable the lifetime of that Singleton 
  // object instance will be until the program completes execution.  The 
  // function will return a reference to this Singleton instance.  So the first
  // time the function is called the instance is created and returned, but then 
  // on all subsequent function calls the SAME instance will be returned! 
  //
  // This approach to managing the creation of a single Singleton object 
  // instance has a coupe advantages.  Static local variables are thread-safe 
  // as of C++11, which means multiple threads can use the Singleton without 
  // the danger of multiple Singleton object instances being created as we 
  // may have with some other approaches to implementing the Singleton 
  // design pattern.  And because it's a static local variable, the Singleton 
  // object instance will not be instantiated UNTIL the function is called. 
  // So if our program NEVER calls get_instance (say because it doesn't need the
  // Singleton), then the Singleton object is NEVER created.  This can be a good
  // property to have because resources like memory will not be used unless they
  // are needed.  We call this property 'lazy instantiation' and its in contrast
  // to 'eager instantiation' where the Singleton object instance is created 
  // in advance.
  static Singleton& get_instance()
  {
    static Singleton instance;
    return instance;
  }
  
  // C++ classes are provided with some special member functions by default such
  // as the copy constructor, move constructor, copy assignment operator, and 
  // move assignment operator.  These member functions would allow for the 
  // creation of additional Singleton object instances if they were to be used,
  // so we use the delete specifier to turn them into deleted functions, i.e. 
  // functions that we explicitly tell the compiler NOT to provide our class.  
  // This way if programmers use our class attempt to use any of these member 
  // functions, a compiler error will result, making our implementation less 
  // error prone and more robust.
  Singleton(const Singleton&) = delete;
  Singleton(Singleton&&) = delete;
  Singleton& operator=(const Singleton&) = delete;
  Singleton& operator=(Singleton&&) = delete;
  
};

int main()
{
  // Because the Singleton constructor is protected, attempting to instantiate
  // a Singleton object outside of the class will result in an error.  Try 
  // uncommenting the below line to see the error.
  //
  // Singleton bad_singleton;
  
  // Use get_instance() to obtain the reference to the Singleton object 
  // instance, because this is the first time get_instance() is used we know 
  // the Singleton object instance is also being created at this point.
  Singleton &singleton1 = Singleton::get_instance();
  
  // singleton1 is a reference to the Singleton object instance, and we can use
  // this reference to set the data member variable
  singleton1.data = 20;
  
  // We can also use the singleton1 reference to output the data member variable
  // of this Singleton object instance
  cout << "singleton1.data = " << singleton1.data << endl;
  
  // If we make another reference to the Singleton object instance by calling 
  // get_instance() again, it will be a reference to the SAME Singleton object.
  // As this is the second time we call get_instance() we know that the 
  // Singleton object instance will have already been created, and now a 
  // reference to that object is being returned.
  Singleton &singleton2 = Singleton::get_instance();
  
  // If we use singleton2 to access and output the data member variable we will
  // again get the value 20 because singleton2 is a reference to the SAME single
  // Singleton object instance in memory.
  cout << "singleton2.data = " << singleton2.data << endl;
  
  // We don't really need to create a reference variable to access the Singleton
  // object, as get_instance() returns a reference we could use the reference 
  // directly as below to access the data member variable...
  Singleton::get_instance().data = 50;
  
  // We again use the reference returned from get_instance() to access the 
  // Singleton object instance and the data member variable, this time to print 
  // it out...
  cout << "data: " << Singleton::get_instance().data << endl;
  
  // And again we'll see if we use the singleton1 or singleton2 references to 
  // access the Singleton object instance that the data member variable will 
  // have the same value because they are both *references* to the *same* 
  // Singleton object instance.
  cout << "singleton1.data = " << singleton1.data << endl;
  cout << "singleton2.data = " << singleton2.data << endl;
  
  // Had we not turned the copy constructor into a deleted function, then the 
  // below code would result in the copy constructor producing a new Singleton 
  // object instance... now instead we'll get a compiler error if we uncomment
  // this code.  We want that compiler error to occur because it prevents a 
  // programmer from incorrectly creating an additional Singleton object 
  // instance (or *mistakenly*, if they forgot the & when trying to make a 
  // reference variable).
  //
  // Singleton singletonN = singleton1;
  
  // If the copy constructor is NOT a deleted function and we CAN use the above
  // statement, a new Singleton object instance will be created.  We can observe
  // this is the case by setting the data member variable of SingletonN, and 
  // noticing it will be a different value then the data member variable for 
  // the Singleton object instance that singleton1 is a reference to. 
  //
  // singletonN.data = 100;
  // cout << "singleton1.data = " << singleton1.data << endl;
  // cout << "singletonN.data = " << singletonN.data << endl;
  
  return 0;
}

# Usage and Benefits

Singletons are commonly used in scenarios such as managing database connections, logging systems, configuration settings, and more. By restricting the instantiation of a class to a single object, the Singleton pattern promotes efficient resource management and simplifies access to shared resources.

# Potential Pitfalls

While Singleton offers numerous benefits, it's essential to be cautious of potential drawbacks, such as:

  1. Global State: Singleton introduces global state, which can make code harder to test and reason about.
  2. Concurrency Issues: In a multithreaded environment, special care must be taken to ensure thread safety during the initialization of the singleton instance.
  3. Lifetime Management: The lifetime of a singleton instance is tied to the lifetime of the application, potentially leading to issues with resource cleanup.

 

The Admin

The Admin

And yet you incessantly stand on their slates, when the White Rabbit: it was YOUR table,' said.