In today's post, I like to touch on a controversial topic: singletons. While I think it is best to have a codebase without singletons, the real-world shows me that singletons are often part of codebases.
Since at least for the code that I see singletons exists, let's make sure you implement them correctly.
A logger singleton
Let's use a usage pattern for a singleton that I see frequently, a system-wide logger. A simple implementation can look like the following code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
The key parts for a singleton in C++ are that the constructor is private and an access function that is static. With that, you ensure that a singleton object, here Logger can only be constructed by calling Instance, essentially limiting the number of Logger objects to a single one.
You're using such a Logger like this:
|
One great part here is, that the implementation of Info and Error can be changed without users knowing. Instead of simply printing to the console, the message can be written to a file or even sent via network. I leave such implementations to your imagination.
For today's focus it is not relevant whether you place the static object theOneAndOnlyLogger inside the Instance function or make it a static private data member of the class.
Is there an issue...
... I mean, besides that you have a singleton in your system?
Yes!
The majority of singleton implementations I saw in the past looked like the one I sketched above. So what's the issue?
Let's take a look at how such a singleton is used when access to the logger is frequently required inside a function or class. People then try to shorten the code, maybe also making it more explicit that they refer to the same singleton each time. The result shows up as the following:
|
Okay, but that's not a problem, right? Yes, that code is fine. But, what about this version:
|
The code compiles and links. It even runs. Did you spot the difference?
I dropped the reference from the logger variable. The new code now leads to a new singleton object and not to a reference to the original. By that singleton doesn't really apply anymore.
While the Logger object implements the most often mentioned key parts of the singleton pattern, one key part wasn't addressed. The implicitly created copy and move operations the compiler kindly creates for use. Of course, in the public section. While you can only create an object of type Logger via the Instance function, once the object is created it can be copied or moved.
In my example, this might not be a problem, but once there is data stored in the object, things get messy and ugly to debug.
A correct implementation of the singleton pattern ensures that the copy- and move-operations are private as well, making the implementation look as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | |
Once things get more complicated, at least I tend to focus on the implementation of the logging functionality and less on the mechanical singleton part. So my advice is to keep the copy- and move-operations in mind when you implement a singleton object.
Andreas