The Singleton Design Pattern is one of the simplest and most widely used design patterns.
It is used in so many different situations that all software developers will come across this pattern at one point or another. What happens though is that a lot of the time, we don’t really know the details of the implementation even when using the pattern. This blog post is aimed at making sure that you understand the Singleton Design Pattern inside out.
It is built up on a simple concept – making sure that a specific class can have only one object (or instance). We can formally define the pattern as “ensuring that only one instance of a class exists and a global point of access to it exists.”
But why do we need this kind of a design pattern?
An example of using the Singleton Design Pattern would be in the logging of an application. You want all parts of your program to log various events and errors onto a single place, and that is where the singleton class helps.
Another example is when you are using a database and require a database connection. Ideally, you would want to have only one access point of the connection, which could be used across all the modules of the program.
The Singleton pattern, as we can see from its formal definition, is made up from two basic principles.
- To ensure that the class only has one instance
- Extend global access to that instance
Let’s see how we can implement both principles to apply the Singleton pattern.
Problem Statement
To understand the Singleton pattern, we will use the classic logging scenario that we mentioned earlier as our problem statement. We will create a simple Logger that may be used by multiple users or modules to log messages.
Basic Approach
- Let’s start by creating a simple logger class in our logger.hpp, which contains a method Log, that will print out a log message.
#ifndef logger_h
#define logger_h
#include <string>
using namespace std;
class Logger
{
public:
Logger();
void Log(string msg);
};
#endif
- Let’s now specify the implementation in our logger.cpp file. We’ll set up our constructor, and the Log() function that prints any message passed to it.
#include <iostream>
#include "logger.hpp"
using namespace std;
Logger::Logger()
{
cout << "New instance created." << endl;
}
void Logger::Log(string msg)
{
cout << msg << endl;
}
- While there are going to be multiple users using the Logger, for the sake of our example we’ll assume there are only 2 users, to keep things simple. So we can just write all our code in the main() function itself.
So in our user.cpp, let’s create 2 loggers in main(), both of which are using our Log() function for our 2 separate users.
#include <iostream>
#include "logger.hpp"
using namespace std;
int main()
{
Logger *logger1 = new Logger();
logger1->Log("this msg is from user 1");
Logger *logger2 = new Logger();
logger2->Log("this msg is from user 2");
return 0;
}
If we were to run this, we’d see that 2 new logger instances are created, one each per user.
As we said earlier, the entire concept of Singleton pattern is based on ensuring that only one instance of the class has been created. To do this, how do you keep track of the number of instances of a class? Let’s take a look at a general technique you can use anywhere, not just for the Singleton design pattern.
- We will make use of a static counter in the class, which is incremented every time the constructor is called. Since this counter is static, it doesn’t belong to a particular object but instead belongs to the class, which ensures that the counter doesn’t get reset.
A static variable is a common property of all objects (i.e., it is not unique for every object). We can use a static integer ‘ctr’ within the logger class, initialized to 0. This keeps track of the number of instances of the class. Every time the constructor is called, the counter is incremented.
logger.hpp
#ifndef logger_h
#define logger_h
#include <string>
using namespace std;
class Logger
{
static int ctr;
public:
Logger();
void Log(string msg);
};
#endif
logger.cpp
#include <iostream>
#include "logger.hpp"
using namespace std;
int Logger::ctr = 0;
Logger::Logger()
{
ctr++;
cout << "New instance created. No of instances " << ctr << endl;
}
void Logger::Log(string msg)
{
cout << msg << endl;
}
Alright, now how do we make this Logger class singleton?
The first thing that comes to mind when we try to ensure that there is only one object of a class, is to restrict the users from making any more objects. We can achieve that by making the default constructor private.
#ifndef logger_h
#define logger_h
#include <string>
using namespace std;
class Logger
{
static int ctr;
Logger();
public:
void Log(string msg);
};
#endif
We can see this change reflected in the user.cpp file where we have an error saying that the logger is inaccessible.
Great! We’ve done it! Right?
Well, not really.
By making the default constructor private, we have restricted all users from creating objects of that class. If we leave it at that, no user can create any object, but we do need that one instance to be able to use the singleton class.
So how can we do that?
- We can do that by creating a static function getLogger() in the Logger class, whose job is to create and return a pointer to an instance of the Logger class. This public function calls the constructor (that we had made private) to create an object and returns a pointer pointing to this newly created instance.
If you’ve already seen implementations of the Singleton design pattern, you may have noticed that they name this function getInstance() regardless of the class. But this is just a misconception that it always has to be getInstance()!
- Now, we do need one object to be returned by this static getLogger(), so we will make a static instance, loggerInstance, for that. This is the single instance that is going to be used by all of our modules and users within our program, and we’re going to keep it private.
- Since our loggerInstance is static, let’s initialise it to null in our logger.cpp file.
- What’s left is to ensure that the getLogger() function can only create a single loggerInstance object, and if an object already exists, it should return the existing object. We can achieve this with a simple if-else check within the getLogger() function.
- And lastly, we’ll change the user.cpp file to use the getLogger() function in main() instead of new Logger().
Here’s what our code finally looks like.
logger.hpp
#ifndef logger_h
#define logger_h
#include <string>
using namespace std;
class Logger
{
static int ctr;
static Logger *loggerInstance;
Logger();
public:
static Logger *getLogger();
void Log(string msg);
};
#endif
logger.cpp
#include <iostream>
#include "logger.hpp"
using namespace std;
int Logger::ctr = 0;
Logger *Logger::loggerInstance = nullptr;
Logger::Logger()
{
ctr++;
cout << "New instance created. No of instances " << ctr << endl;
}
void Logger::Log(string msg)
{
cout << msg << endl;
}
Logger *Logger::getLogger()
{
if (loggerInstance == nullptr)
{
loggerInstance = new Logger();
}
return loggerInstance;
}
user.cpp
#include <iostream>
#include "logger.hpp"
using namespace std;
int main()
{
Logger *logger1 = Logger::getLogger();
logger1->Log("this msg is from user 1");
Logger *logger2 = Logger::getLogger();
logger2->Log("this msg is from user 2");
return 0;
}
In our output, we can see that there’s only one instance that’s been created!
We’ve now successfully wrapped up our Singleton Design Pattern implementation… or have we? Have you ever heard of thread-safety?
Code failure in Multi-threading
Multi-threading can introduce several problems if the code is not designed with it in mind. The code that we have written till now, it was perfectly fine and useable in case of a single thread but fails to meet our exact requirements when multi-threading is present.
How does multi-threading affect the results of our code?
Well, let us suppose we have 2 threads. Thread 1 and thread 2 both check and proceed from the if statement at the same time, before the other thread can execute the next line of code. Now both thread 1 and thread 2 have passed the check that we had added, and both can create a new instance of the class on the next line of code, violating the principles of the simpleton pattern. We now have 2 separate instances!
To demonstrate how this looks in our code, we’re going to create 2 separate functions user1logs() and user2logs() which will be called by 2 separate threads (just to make it easier to showcase the situation). We’ll create our threads in main() and have them call the 2 new functions, after which we’ll join both threads.
#include <iostream>
#include "logger.hpp"
#include <thread>
using namespace std;
void user1logs()
{
Logger *logger1 = Logger::getLogger();
logger1->Log("this msg is from user 1");
}
void user2logs()
{
Logger *logger2 = Logger::getLogger();
logger2->Log("this msg is from user 2");
}
int main()
{
thread t1(user1logs);
thread t2(user2logs);
t1.join();
t2.join();
return 0;
}
What does our output look like? We can see that the order of messages printed cannot be predicted, with the number of instances varying between 1 and 2.
With multi-threading, we can never really tell which thread runs first or creates an instance first!
To solve such an issue, we must make sure that when one thread is accessing a block of code, the other thread gets blocked from accessing it.
In our case, the block of code we are talking about is the if-else block that performs the check and creates the instance. To do that, we use a mutex lock and its lock() and unlock() functions around the if-else block. We’ll declare our mutex lock in logger.hpp, and define and use it in logger.cpp.
logger.hpp
#ifndef logger_h
#define logger_h
#include <string>
#include <mutex>
using namespace std;
class Logger
{
static int ctr;
static Logger *loggerInstance;
static mutex mtx;
Logger();
public:
static Logger *getLogger();
void Log(string msg);
};
#endif
logger.cpp
#include <iostream>
#include "logger.hpp"
using namespace std;
int Logger::ctr = 0;
Logger *Logger::loggerInstance = nullptr;
mutex Logger::mtx;
Logger::Logger()
{
ctr++;
cout << "New instance created. No of instances " << ctr << endl;
}
void Logger::Log(string msg)
{
cout << msg << endl;
}
Logger *Logger::getLogger()
{
mtx.lock();
if (loggerInstance == nullptr)
{
loggerInstance = new Logger();
}
mtx.unlock();
return loggerInstance;
}
Now when we run it, we can see that the number of instances of the Logger created is always 1.
When the first thread is creating the instance, the lock ensures that thread 2 cannot access the if-else block, resulting in a single instance being created even in multi-threading.
Fantastic! We’re finally done with the Singleton Design Pattern implementation.
Or are we?
Double Checked Locking
We need the mutex lock to ensure that only one thread at a time gets access and can create the object. But when we have created an instance once, we do not need to use the lock again.
So, we add another check, which will confirm if an object has already been created before putting the lock.
If it has been created, then we do not need to bother with the mutex lock and the code for the creation of the object at all, we just return the object. If not, we then proceed with the mutex lock and the code to create the object. This reduces the overhead of using the mutex lock drastically.
Logger *Logger::getLogger()
{
if (loggerInstance == nullptr)
{
mtx.lock();
if (loggerInstance == nullptr)
{
loggerInstance = new Logger();
}
mtx.unlock();
}
return loggerInstance;
}
Conclusion
Let’s recap everything we just did:
- Restricted users from creating objects of a class by making the default constructor private
- Created a static object and a function which returns an instance
- Added a check within the function to ensure only one instance gets created
- Added a mutex lock to make sure our code is thread-safe
- Implemented Double Checked Locking, ensuring that the mutex lock gets used only during the initial object creation, and not from then onwards, bringing down our lock expenses.
And finally, finally we’re done!
Well, almost.
There are still a few cases we haven’t looked at, one of which is the fact that the default constructor isn’t the only way to call the constructor. So, we’ll need to make the copy constructor private as well as make the ‘=’ operator overloading as private in our logger.hpp.
class Logger
{
static int ctr;
static Logger *loggerInstance;
static mutex mtx;
Logger();
Logger(const Logger &);
Logger operator=(const Logger &);
public:
static Logger *getLogger();
void Log(string msg);
};
For C++, starting from C++ 11, making these functions private isn’t the only way to restrict calling them from users. We can use the ‘=delete’ feature to prevent users from calling functions even if they’re declared public.
Singleton is a particularly important design pattern ensuring a single object, which leads to better security and performance. However, we must always check whether the pattern is suitable for a program or not, and make sure we’re looking at all the different ways in which a user might attempt to create multiple instances of the class, and prevent them from doing so.
Would you like to watch a video walking you through this Singleton Pattern implementation? Check out our video on the official YouTube channel!