In this chapter, we take a look at the (in-)famous Singleton pattern. I know, you may already be acquainted with Singleton, and you may already have a strong opinion about it. It is even possible that you consider Singleton an antipattern and thus ask yourself how I mustered the courage to include it in this book. Well, I am aware that Singleton is not particularly popular and in many circles has a rather bad reputation, in particular because of the global nature of Singletons. From that perspective, however, it might be very surprising to learn that there are a couple of “Singleton”-like instances in the C++ Standard Library. Seriously! And, honestly, they work fantastically! Therefore, we should seriously talk about what a Singleton is, when Singleton works, and how to deal with Singleton properly.
In “Guideline 37: Treat Singleton as an Implementation Pattern, Not a Design Pattern”, I will explain the Singleton pattern and demonstrate how it works by a very commonly used implementation, the so-called Meyers’ Singleton. I will, however, also make a strong argument to not treat Singleton as a design pattern but as an implementation pattern.
In “Guideline 38: Design Singletons for Change and Testability”, we accept the fact that sometimes we need a solution to represent the few global aspects in our code. This is what the Singleton pattern is often used for. This also means that we are confronted by the usual problems of Singletons: global state; many strong, artificial dependencies; and an impeded changeability and testability. While these sound like excellent reasons to avoid Singleton after all, I will show you that by proper software design, you can combine the Singleton benefits with excellent changeability and testability.
Let me start by addressing the elephant in the room:
Singleton is not a design pattern.
If you haven’t heard about Singleton before, then this might not make any sense at all, but bear with me. I promise to explain Singleton shortly. If you have heard about Singleton before, then I assume you’re either nodding in agreement with a sympathizing “I know” look on your face, or you are utterly stunned and initially don’t know what to say. “But why not?” you eventually dare to ask. “Isn’t it one of the original design patterns from the Gang of Four book?” Yes, you’re correct: Singleton is one of the 23 original patterns documented in the GoF book. At the time of writing, Wikipedia calls it a design pattern, and it is even listed as a design pattern in Steve McConnell’s bestseller Code Complete.1 Nevertheless, it still isn’t a design pattern, because it doesn’t have the properties of a design pattern. Let me explain.
Sometimes you may want to guarantee that there is only one, and exactly one, instance of a particular class. In other words, you have a Highlander situation: “There can be only one.”2 This might make sense for the system-wide database, the one and only logger, the system clock, the system configuration, or, in short, any class that should not be instantiated multiple times, since it represents something that exists only once. That is the intent of the Singleton pattern.
Intent: “Ensure a class has only one instance, and provide a global point of access to it.”3
This intent is visualized by the Gang of Four with the UML diagram in
Figure 10-1, which introduces the instance()
function as the global
point of access to the unique instance.
There are multiple ways to restrict the number of instantiations to exactly one.
Definitely one of the most useful and therefore most commonly used forms of Singleton is
the Meyers’ Singleton.4 The following Database
class is implemented as a Meyers’ Singleton:
//---- <Database.h> ----------------
class
Database
final
{
public
:
static
Database
&
instance
(
)
{
static
Database
db
;
// The one, unique instance
return
db
;
}
bool
write
(
/*some arguments*/
)
;
bool
read
(
/*some arguments*/
)
const
;
// ... More database-specific functionality
// ... Potentially access to data members
private
:
Database
(
)
{
}
Database
(
Database
const
&
)
=
delete
;
Database
&
operator
=
(
Database
const
&
)
=
delete
;
Database
(
Database
&
&
)
=
delete
;
Database
&
operator
=
(
Database
&
&
)
=
delete
;
// ... Potentially some data members
}
;
The Meyers’ Singleton evolves around the fact that it’s possible to access the
single instance of the Database
class onlhy via the public
, static
instance()
function
():
#include
<Database.h>
#include
<cstdlib>
int
main
()
{
// First access, database object is created
Database
&
db1
=
Database
::
instance
();
// ...
// Second access, returns a reference to the same object
Database
&
db2
=
Database
::
instance
();
assert
(
&
db1
==
&
db2
);
return
EXIT_SUCCESS
;
}
Indeed, this function is the only way to get a Database
: all functionality that could
possibly be used to create, copy, or move an instance is either declared in the private
section or is explicitly delete
d.5
Although this appears to be pretty straightforward, one implementation detail is
of special interest: note that the default constructor is explicitly defined and not
default
ed
().
The reason is if it were
default
ed, up to
C++17, it would be
possible to create a Database
with an empty set of braces, i.e., via
value initialization:
#include
<cstdlib>
class
Database
{
public
:
// ... As before
private
:
Database
()
=
default
;
// Compiler generated default constructor
// ... As before
};
int
main
()
{
Database
db
;
// Does not compile: Default initialization
Database
db
{};
// Works, since value initialization results in aggregate
// initialization, because Database is an aggregate type
return
EXIT_SUCCESS
;
}
Up to C++17, the Database
class counts as an aggregate type, which
means that value initialization would be performed via
aggregate initialization.
Aggregate initialization, in turn, ignores the default constructor, including
the fact that it is private
, and simply performs a zero initialization of the object.
Thus, value initialization enables you to still create an instance. If, however,
you provide the default constructor, then the class does not count as an aggregate
type, which prevents aggregate initialization.6
The instance()
function is implemented in terms of a
static local variable.
This means that the first time control passes through the declaration, the variable is
initialized in a thread-safe way, and on all further calls the initialization is skipped.7 On every call, the first and all subsequent calls, the function returns a reference to the static local variable.
The rest of the Database
class is pretty much what you would expect from a class
representing a database: there are some public
, database-related functions (e.g., write()
and read()
) and there could be some data members, including access functions. In
other words, except for the instance()
member function and the special members,
Database
is just a normal class.
Now, with one possible implementation of a Singleton in mind, let’s go back to my claim that Singleton is not a design pattern. First, let’s remind ourselves of the properties of a design pattern, which I defined in “Guideline 11: Understand the Purpose of Design Patterns”:
A design pattern:
Has a name
Carries an intent
Introduces an abstraction
Has been proven
The Singleton pattern definitely has a name, and it definitely has an intent. No question there. I would also claim that it has been proven over the years (although there may be skeptical voices that point out that Singleton is rather infamous). However, there is no kind of abstraction: no base class, no template parameters, nothing. Singleton does not represent an abstraction itself, and it does not introduce an abstraction. In fact, it isn’t concerned with the structure of code or with the interaction and interdependencies of entities, and hence it isn’t aiming at managing or reducing dependencies.8 This, though, is what I defined to be an integral part of software design. Instead, Singleton is focused on restricting the number of instantiations to exactly one. Thus, Singleton is not a design pattern but merely an implementation pattern.
“Then why is it listed as a design pattern in so many important sources?” you ask. This is a fair and good question. There may be three answers to that. First, in other programming languages, in particular languages where every class can automatically represent an abstraction, the situation may be different. While I acknowledge this, I still believe that the intent of the Singleton pattern is primarily targeted for implementation details and not for dependencies and decoupling.
Second, Singleton is very commonly used (although often also misused), so it is definitely a pattern. Since there are Singletons in many different programming languages, it does not appear to be just an idiom of the C++ programming language. As a consequence, it appears reasonable to call it a design pattern. This chain of arguments may sound plausible to you, but I feel it falls short of distinguishing between software design and implementation details. This is why in “Guideline 11: Understand the Purpose of Design Patterns”, I introduced the term implementation pattern to distinguish between different kinds of language-agnostic patterns such as Singleton.9
And third, I believe that we are still in the process of understanding software design and design patterns. There is no common definition of software design. For that reason, I came up with one in “Guideline 1: Understand the Importance of Software Design”. There is no common definition of design patterns, either. This is why I came up with one in “Guideline 11: Understand the Purpose of Design Patterns”. I strongly believe that we must talk more about software design and more about patterns to come to a common understanding of the necessary terminology, especially in C++.
In summary, you do not use a Singleton to decouple software entities. So despite the fact that it is described in the famous GoF book, or in Code Complete, or even listed as a design pattern on Wikipedia, it does not serve the purpose of a design pattern. Singleton is merely dealing with implementation details, and as such you should treat it as an implementation pattern.
Singleton is indeed a rather infamous pattern: there are many voices out there that describe Singleton as a general problem in code, as an antipattern, as dangerous, or even as evil. Therefore, there is a lot of advice out there to avoid the pattern, among others, Core Guideline I.3:10
Avoid singletons.
One of the primary reasons why people dislike Singleton is that it often causes artificial dependencies and obstructs testability. As such, it runs contrary to two of the most important and most general guidelines in this book: “Guideline 2: Design for Change” and “Guideline 4: Design for Testability”. From that perspective, Singleton indeed appears to be a problem in code and should be avoided. However, despite all the good-intentioned warnings, the pattern is persistently used by many developers. The reasons for that are manifold but probably mainly related to two facts: first, sometimes (and let’s agree on sometimes) it is desirable to express the fact that something exists only once and should be available for many entities in the code. Second, sometimes Singleton appears to be the proper solution, as there are global aspects to represent.
So, let’s do the following: instead of arguing that Singleton is always bad and evil, let’s focus on those few situations where we need to represent a global aspect in our program and discuss how to represent this aspect properly, but still design for change and testability.
Singletons are mostly used to represent entities in a program that logically and/or physically exist only once and that should be used by many other classes and functions.11 Common examples are the system-wide database, logger, clock, or configuration. These examples, including the term system-wide, give an indication of the nature of these entities: they commonly represent globally available functionality or data, i.e., global state. From that perspective, the Singleton pattern appears to make sense: by preventing everyone from creating new instances, and by forcing everyone to use the one instance, you can guarantee uniform and consistent access to this global state across all using entities.
This representation and introduction of global state, however, explains why Singleton is commonly considered a problem. As Michael Feathers expressed it:12
The singleton pattern is one of the mechanisms people use to make global variables. In general, global variables are a bad idea for a couple of reasons. One of them is opacity.
Global variables are indeed a bad idea, particularly for one important reason: the term variable suggests that we are talking about mutable global state. And that kind of state can indeed cause a lot of headaches. To be explicit, mutable global state is frowned upon (in general, but especially in a multithreaded environment), as it is difficult, costly, and likely both to control access and guarantee correctness. Furthermore, global (mutable) state is very hard to reason about, as read and write access to this state usually happens invisibly within some function, which, based on its interface, does not reveal the fact that it uses the global state. And last but not least, if you have several globals, whose lifetimes depend on one another and that are distributed over several compilation units, you might be facing the static initialization order fiasco (SIOF).13 Obviously, it is beneficial to avoid global state as much as possible.14
The problem of global state, however, is a problem that we can’t resolve by avoiding Singletons. It’s a general problem, unrelated to any particular pattern. The same problem, for instance, also exists for the Monostate pattern, which enforces a single, global state but allows for any number of instantiations.15 So on the contrary, Singleton can help deal with the global state by constraining access to it. For instance, as Miško Hevery explains in his 2008 article, Singletons that provide a unidirectional data flow to or from some global state are acceptable:16 a Singleton implementing a logger would only allow you to write data but not read it. A Singleton representing a system-wide configuration or clock would only allow you to read the data but not write it, thus representing a global constant. The restriction to unidirectional data flow helps avoid many of the usual problems with global state. Or in the words of Miško Hevery (the emphasis being mine):17
Appropriate use of “Global” or semi-Global states can greatly simplify the design of applications […].
Global state is an intrinsic problem of Singletons. However, even if we feel comfortable
with representing global state with a Singleton, there are serious consequences:
functions that use Singletons depend on the represented global data and thus become
harder to change and harder to test. To better understand this, let’s revive the
Database
Singleton from “Guideline 37: Treat Singleton as an Implementation Pattern, Not a Design Pattern”, which
is now actively used by a couple of arbitrary classes, namely Widget
and Gadget
:
//---- <Widget.h> ----------------
#include
<Database.h>
class
Widget
{
public
:
void
doSomething
(
/*some arguments*/
)
{
// ...
Database
::
instance
().
read
(
/*some arguments*/
);
// ...
}
};
//---- <Gadget.h> ----------------
#include
<Database.h>
class
Gadget
{
public
:
void
doSomething
(
/*some arguments*/
)
{
// ...
Database
::
instance
().
write
(
/*some arguments*/
);
// ...
}
};
Widget
and Gadget
both require access to the system-wide Database
. For that reason, they
call the Database::instance()
function and subsequently the read()
and write()
functions.
Since they use the Database
and thus depend on it, we would like them to reside in
architecture levels below the level of the Database
Singleton. That is because,
as you remember from “Guideline 2: Design for Change”, we can call it a proper architecture
only if all dependency arrows run toward the high levels (see Figure 10-2).
Database
implemented as a SingletonAlthough this dependency structure may be desirable, unfortunately it is only an
illusion: the Database
class is not an abstraction but a concrete implementation,
representing the dependency on a very specific database! Therefore, the real
dependency structure is inverted and looks something like Figure 10-3.
The actual dependency structure utterly fails the Dependency Inversion Principle (DIP) (see “Guideline 9: Pay Attention to the Ownership of Abstractions”): all dependency arrows point toward the lower level. In other words, right now there is no software architecture!
Database
implemented as a SingletonSince the Database
is a concrete class and not an abstraction, there are strong and
unfortunately even invisible dependencies from all over the code to the specific
implementation details and design choices of the Database
class. This may—in the
worst case—include a dependency on vendor-specific details that become visible
throughout the code, manifest in many different places, and later make changes
excruciatingly hard or even impossible. Due to that, the code becomes much more
difficult to change.
Also consider how badly tests are affected by this dependency. All tests that use
one of the functions depending on the Database
Singleton become themselves dependent
on the Singleton. This means, for instance, that for every test using the
Widget::doSomething()
function, you would always have to provide the one and only
Database
class. The unfortunate, but also simple, reason is that none of these functions
provide you with a way to substitute the Database
with something else: any kind of stub,
mock, or fake.18
They all treat the Database
Singleton as their shiny, precious secret.
Testability is therefore severely impeded, and writing tests becomes so much harder that
you might be tempted to not write them at all.19
This example indeed demonstrates the usual problems with Singletons and the unfortunate
artificial dependencies they introduce. These dependencies make the system more inflexible and
more rigid, and thus harder to change and test. That, of course, should not be.
On the contrary, it should be easy to replace a database implementation with another one,
and it should be easy to test functionality that uses a database. For these exact reasons,
we must make sure that the Database
becomes a true implementation detail on the low level
of a proper architecture.20
“But wait a second, you just said that if the Database
is an implementation detail, there
is no architecture, right?” Yes, I said that. And there is nothing we can do as it is: the
Database
Singleton does not represent any abstraction and does not enable us to deal
with dependencies at all. Singleton is just not a design pattern. So in order to remove the
dependencies on the Database
class and make the architecture work, we will have
to design for change and testability by introducing an abstraction and using a
real design pattern. To achieve that, let’s take a look at an example with a good
way to deal with global aspects, using Singletons from the C++ Standard
Library.
I’m returning to a true El Dorado of design patterns, which I have used several times to demonstrate different design patterns: the C++17 polymorphic memory resources:
#
include
<array>
#
include
<cstddef>
#
include
<cstdlib>
#
include
<memory_resource>
#
include
<string>
#
include
<vector>
// ...
int
main
(
)
{
std
:
:
array
<
std
:
:
byte
,
1000
>
raw
;
// Note: not initialized!
std
:
:
pmr
:
:
monotonic_buffer_resource
buffer
{
raw
.
data
(
)
,
raw
.
size
(
)
,
std
:
:
pmr
:
:
null_memory_resource
(
)
}
;
std
:
:
pmr
:
:
vector
<
std
:
:
pmr
:
:
string
>
strings
{
&
buffer
}
;
// ...
return
EXIT_SUCCESS
;
}
In this example, we configure the
std::pmr::monotonic_buffer_resource
,
called buffer
, to work only with the static memory contained in the given std::array
raw
().
If this memory is depleted,
buffer
will try to acquire new memory via its upstream
allocator, which we specify to be
std::pmr::null_memory_resource()
.
Allocating via this allocator will never return any memory but will always fail with the
std::bad_alloc()
exception. Thus, buffer
is restricted to the 1,000 bytes provided by raw
.
While you should immediately remember and recognize this as an example of the Decorator
design pattern, this also serves as an example of the Singleton pattern: the
std::pmr::null_memory_resource()
function returns a pointer to the same allocator
every time the function is called and thus acts as a single point of access to the
one and only instance of std::pmr::null_memory_resource
. Thus, the returned allocator
acts as a Singleton. Although this Singleton does not provide a unidirectional flow of
data (after all, we can both allocate memory and give it back), Singleton still feels like
a reasonable choice, as it represents one kind of global state: memory.
It is particularly interesting and important to note that this Singleton does
not make you depend on the specific implementation details of the allocator. Quite the
opposite: the std::pmr::null_memory_resource()
function returns a pointer to
std::pmr::memory_resource
.
This class represents a base class for all kinds of allocators (at least in the realm
of C++17), and thus serves as an abstraction. Still,
std::pmr::null_memory_resource()
represents a specific allocator, a specific choice,
which we now depend on. As this functionality is in the Standard Library, we tend to
not recognize it as a dependency, but generally speaking it is: we are not provided
with an opportunity to replace the standard-specific
implementation.
This changes if we replace the call to std::pmr::null_memory_resource()
with a call to
std::pmr::get_default_resource()
():
#
include
<memory_resource>
// ...
int
main
(
)
{
// ...
std
:
:
pmr
:
:
monotonic_buffer_resource
buffer
{
raw
.
data
(
)
,
raw
.
size
(
)
,
std
:
:
pmr
:
:
get_default_resource
(
)
}
;
// ...
return
EXIT_SUCCESS
;
}
The std::pmr::get_default_resource()
function also returns a pointer to
std::pmr::memory_resource
, which represents an abstraction for the system-wide default
allocator. By default, the returned allocator is returned by the
std::new_delete_resource()
function. However, amazingly, this default can be customized by the
std::pmr::set_default_resource()
function:
namespace
std
::
pmr
{
memory_resource
*
set_default_resource
(
memory_resource
*
r
)
noexcept
;
}
// namespace std::pmr
With this function, we can define the std::pmr::null_memory_resource()
as the new
system-wide default allocator
():
// ...
int
main
(
)
{
// ...
std
:
:
pmr
:
:
set_default_resource
(
std
:
:
pmr
:
:
null_memory_resource
(
)
)
;
std
:
:
pmr
:
:
monotonic_buffer_resource
buffer
{
raw
.
data
(
)
,
raw
.
size
(
)
,
std
:
:
pmr
:
:
get_default_resource
(
)
}
;
// ...
return
EXIT_SUCCESS
;
}
With std::pmr::set_default_resource()
, you are able to customize the system-wide
allocator. In other words, this function provides you with the ability to inject the
dependency on this allocator. Does this ring a bell? Does this sound familiar?
I very much hope this makes you think about another, essential design pattern…drum roll…yes, correct: the Strategy design pattern.21
Indeed, this is a Strategy. Using this design pattern is a fantastic choice,
because it has an amazing effect on the architecture. While
std::pmr::memory_resource
represents an abstraction from all possible allocators
and thus can reside on the high level of the architecture, any concrete implementation
of an allocator, including all (vendor-)specific implementation details, can reside
on the lowest level of the architecture. As a demonstration, consider this sketch of the
CustomAllocator
class:
//---- <CustomAllocator.h> ----------------
#include
<memory_resource>
class
CustomAllocator
:
public
std
::
pmr
::
memory_resource
{
public
:
// There is no need to enforce a single instance
CustomAllocator
(
/*...*/
);
// No explicitly declared copy or move operations
private
:
void
*
do_allocate
(
size_t
bytes
,
size_t
alignment
)
override
;
void
do_deallocate
(
void
*
ptr
,
size_t
bytes
,
size_t
alignment
)
override
;
bool
do_is_equal
(
std
::
pmr
::
memory_resource
const
&
other
)
const
noexcept
override
;
// ...
};
Note that CustomAllocator
public
ly inherits from std::pmr::memory_resource
in
order to qualify as a C++17 allocator. Due to that, you can establish an instance
of CustomAllocator
as the new system-wide default allocator with the
std::pmr::set_default_resource()
function
():
#
include
<CustomAllocator.h>
int
main
(
)
{
// ...
CustomAllocator
custom_allocator
{
/*...*/
}
;
std
:
:
pmr
:
:
set_default_resource
(
&
custom_allocator
)
;
// ...
}
While the std::pmr::memory_resource
base class resides on the highest level of the
architecture, CustomAllocator
is logically introduced on the lowest architectural
level (see Figure 10-4). Thus, the Strategy pattern causes an
inversion of
dependencies (see “Guideline 9: Pay Attention to the Ownership of Abstractions”):
despite the Singleton-ness of the allocators, despite representing global state,
you depend on an abstraction instead of the concrete implementation details.
std::pmr::memory_resource
abstractionAs a side note, it’s worth pointing out that with this approach you can trivially avoid any dependency on the order of initialization of globals (i.e., SIOF), since you can explicitly manage the initialization order by creating all Singletons on the stack and in a single compilation unit:
int
main
()
{
// The one and only system-wide clock has no lifetime dependencies.
// Thus it is created first
SystemClock
clock
{
/*...*/
};
// The one and only system-wide configuration depends on the clock.
SystemConfiguration
config
{
&
clock
,
/*...*/
};
// ...
}
Based on this previous example, you should now have an idea how to fix our
Database
example. As a reminder, the goal is to keep the Database
class as the default database
implementation but to make it an implementation detail, i.e., to remove all dependencies
on the concrete implementation. All you need to do is apply the
Strategy design
pattern to introduce an abstraction, alongside a global point of access and a global point
for dependency injection, on the high level of our architecture. This will enable anyone
(and I really mean anyone, as you also follow the Open-Closed Principle (OCP); see
“Guideline 5: Design for Extension”) to introduce a custom database implementation (both concrete
implementations as well as test stubs, mocks, or fakes) on the lowest level.
So let’s introduce the following PersistenceInterface
abstraction
():
//---- <PersistenceInterface.h> ----------------
class
PersistenceInterface
{
public
:
virtual
~
PersistenceInterface
(
)
=
default
;
bool
read
(
/*some arguments*/
)
const
{
return
do_read
(
/*...*/
)
;
}
bool
write
(
/*some arguments*/
)
{
return
do_write
(
/*...*/
)
;
}
// ... More database specific functionality
private
:
virtual
bool
do_read
(
/*some arguments*/
)
const
=
0
;
virtual
bool
do_write
(
/*some arguments*/
)
=
0
;
}
;
PersistenceInterface
*
get_persistence_interface
(
)
;
void
set_persistence_interface
(
PersistenceInterface
*
persistence
)
;
// Declaration of the one 'instance' variable
extern
PersistenceInterface
*
instance
;
The PersistenceInterface
base class provides the interface for all possible database
implementations. For instance, it introduces a read()
and a write()
function, split
into the public
interface part and the private
implementation part, based on the
example set by the std::pmr::memory_resource
class
( and
).22
Of course, in reality it would introduce a few more database-specific functions, but let
read()
and write()
be sufficient for this example.
In addition to the PersistenceInterface
, you would also introduce a global point of
access called get_persistence_interface()
()
and a function to enable dependency injection called
set_persistence_interface()
().
These two functions allow you to access and set the global persistence system
(
).
The Database
class now inherits from the PersistenceInterface
base class and implements
the required interface (hopefully adhering to the Liskov Substitution Principle (LSP);
see “Guideline 6: Adhere to the Expected Behavior of Abstractions”):
//---- <Database.h> ----------------
class
Database
:
public
PersistenceInterface
{
public
:
// ... Potentially access to data members
// Make the class immobile by deleting the copy and move operations
Database
(
Database
const
&
)
=
delete
;
Database
&
operator
=
(
Database
const
&
)
=
delete
;
Database
(
Database
&&
)
=
delete
;
Database
&
operator
=
(
Database
&&
)
=
delete
;
private
:
bool
do_read
(
/*some arguments*/
)
const
override
;
bool
do_write
(
/*some arguments*/
)
override
;
// ... More database-specific functionality
// ... Potentially some data members
};
In our special setting, the Database
class represents the default database implementation.
We need to create a default instance of the database, in case no other persistence
system is specified via the set_persistence_interface()
function. However, if
any other persistence system is established as the system-wide database before Database
is created, we must not create an instance, as this would cause unnecessary and unfortunate
overhead. This behavior is achieved by implementing the get_persistence_interface()
function
with two static local variables and an Immediately Invoked Lambda Expression (IILE)
():
//---- <PersistenceInterface.cpp> ----------------
#
include
<Database.h>
// Definition of the one 'instance' variable
PersistenceInterface
*
instance
=
nullptr
;
PersistenceInterface
*
get_persistence_interface
(
)
{
// Local object, initialized by an
// 'Immediately Invoked Lambda Expression (IILE)'
static
bool
init
=
[
]
(
)
{
if
(
!
instance
)
{
static
Database
db
;
instance
=
&
db
;
}
return
true
;
// or false, as the actual value does not matter.
}
(
)
;
// Note the '()' after the lambda expression. This invokes the lambda.
return
instance
;
}
void
set_persistence_interface
(
PersistenceInterface
*
persistence
)
{
instance
=
persistence
;
}
The first time the execution flow enters the get_persistence_interface()
function, the
init
static local variable is initialized. If, at this point in time, the instance
is
already set, no Database
is created. However, if it is not, the Database
instance is
created as another static local variable inside the lambda and bound to the instance
variable:
#include
<PersistenceInterface.h>
#include
<cstdlib>
int
main
()
{
// First access, database object is created
PersistenceInterface
*
persistence
=
get_persistence_interface
();
// ...
return
EXIT_SUCCESS
;
}
This implementation achieves the desired effect: Database
becomes an implementation
detail, which no other code depends on and which can be replaced at any time by a custom database implementation (see Figure 10-5). Thus, despite the
Singleton-ness of Database
, it does not introduce dependencies, and it can be easily
changed and easily replaced for testing purposes.
Database
“Wow, this is a great solution. I bet I can use that in a few places in my own codebase!” you say, with an impressed and appreciative look on your face. “But I see a potential problem: since I have to inherit from an interface class, this is an intrusive solution. What should I do if I can’t change a given Singleton class?” Well, in that case you have two nonintrusive design patterns to choose from. Either you already have an inheritance hierarchy in place, in which case you can introduce an Adapter to wrap the given Singleton (see “Guideline 24: Use Adapters to Standardize Interfaces”), or you don’t have an inheritance hierarchy in place yet, in which case you can put the External Polymorphism design pattern to good use (see “Guideline 31: Use External Polymorphism for Nonintrusive Runtime Polymorphism”).
“OK, but I see another, more serious problem: is this code truly thread-safe?”
Honestly, no, it is not. To give one example for a possible problem: it could happen that
during the first call to get_persistence_interface()
, which may take some time due to the
setup of the Database
instance, the set_persistence_interface()
is called. In that case,
either the Database
is created in vain or the call to
set_persistence_interface()
is
lost. However, perhaps surprisingly, this is not something that we need to address. Here’s
why: remember that the instance
represents global state. If we assume that
set_persistence_interface()
can be called from anywhere in the code at any time, in
general we can’t expect that after calling
set_persistence_interface()
, a call to
get_persistence_interface()
would return the set value. Hence, calling the
set_persistence_interface()
function from anywhere in the code is like pulling the rug
from under somebody’s feet. This is comparable to calling std::move()
on any lvalue:
template
<
typename
T
>
void
f
(
T
&
value
)
{
// ...
T
other
=
std
::
move
(
value
);
// Very bad move (literally)!
// ...
}
From this perspective, the set_persistence_interface()
function should be used at the
very beginning of the program or at the beginning of a single test, not arbitrarily.
“Shouldn’t we make sure that the set_persistence_interface()
function can be
called only once?” you ask. We most certainly could do that, but this would artificially
limit its use for testing purposes: we would not be able to reset the persistence system
at the beginning of every single test.
“OK, I see. One last question: since this solution involves global state that can be changed,
wouldn’t it be better to use a more direct and more local dependency injection to the
lower-level classes? Consider the following modification of the Widget
class, which is
given its dependency upon construction:”
//---- <Widget.h> ----------------
#include
<PersistenceInterface.h>
class
Widget
{
public
:
Widget
(
PersistenceInterface
*
persistence
)
// Dependency injection
:
persistence_
(
persistence
)
{}
void
doSomething
(
/*some arguments*/
)
{
// ...
persistence_
->
read
(
/*some arguments*/
);
// ...
}
private
:
PersistenceInterface
*
persistence_
{};
};
I completely agree with you. This may be the next step to address the problem of global
state. However, before we analyze this approach, keep in mind that this idea is
only an option since we have already inverted the dependencies. Thanks to introducing
an abstraction in the high level of our architecture, we suddenly have choices and can
talk about alternative solutions. Hence, the first and most important step is to properly
manage the dependencies. But back to your suggestion: I really like the approach. The
interface of the Widget
class becomes more “honest” and clearly displays all of its
dependencies. And since the dependency is passed via the constructor argument, the dependency
injection becomes more intuitive and more natural.
Alternatively, you could pass the dependency on the Widget::doSomething()
function
directly:
//---- <Widget.h> ----------------
#include
<PersistenceInterface.h>
class
Widget
{
public
:
void
doSomething
(
PersistenceInterface
*
persistence
,
/*some arguments*/
)
{
// ...
persistence
->
read
(
/*some arguments*/
);
// ...
}
};
While this approach may not be the best for a member function, this may be your only option for free functions. And again, the function becomes a little more “honest” by explicitly stating its dependencies.
However, there is a flip side to this direct dependency injection: this approach may
quickly become unwieldy in large call stacks. Passing a dependency through several levels
of your software stack to make them available at the point they are needed is neither
convenient nor intuitive. Additionally, especially in the presence of several Singletons,
the solution quickly becomes cumbersome: passing, for instance, a
PersistenceInterface
,
an Allocator
, and the system-wide Configuration
through many layers of function calls
just to be able to use them on the lowest level truly is not the most elegant approach.
For that reason, you may want to combine the ideas of providing a global access point and a local dependency injection, for instance, by
introducing a wrapper function:
//---- <Widget.h> ----------------
#
include
<PersistenceInterface.h>
class
Widget
{
public
:
void
doSomething
(
/*some arguments*/
)
{
doSomething
(
get_persistence_interface
(
)
,
/*some arguments*/
)
;
}
void
doSomething
(
PersistenceInterface
*
persistence
,
/*some arguments*/
)
{
// ...
persistence
-
>
read
(
/*some arguments*/
)
;
// ...
}
}
;
While we still provide the previous doSomething()
function
(),
we now additionally provide an overload that accepts a
PersistenceInterface
as a function
argument
().
The second function does all the work, whereas the first function now merely acts as a
wrapper, which injects the globally set
PersistenceInterface
. In this combination,
it’s possible to make local decisions and to locally inject the desired dependency,
but at the same time it is not necessary to pass the dependency through many layers
of function calls.
However, truth be told, while these solutions may work very well in this database example and also in the context of managing memory, it might not be the right approach for every single Singleton problem. So don’t believe that this is the only possible solution. After all, it depends. However, it is a great example of the general process of software design: identify the aspect that changes or causes dependencies, then separate concerns by extracting a fitting abstraction. Depending on your intent, you will just have applied a design pattern. So consider naming your solution accordingly, and by that leave traces of your reasoning for others to pick up on.
In summary, the Singleton pattern certainly is not one of the glamorous patterns. It simply comes with too many disadvantages, most importantly the usual flaws of global state. But still, despite the many negative aspects, if used judiciously, Singleton can be the right solution for representing the few global aspects in your code in some situations. If it is, prefer Singletons with unidirectional data flow, and design your Singletons for change and testability by inverting the dependencies and enabling dependency injection with the Strategy design pattern.
1 Steve McConnell, Code Complete: A Practical Handbook of Software Construction, 2nd ed. (Microsoft Press, 2004).
2 “There can be only one” is the tagline of the 1986 movie Highlander featuring Christopher Lambert.
3 Erich Gamma et al., Design Patterns: Elements of Reusable Object-Oriented Software.
4 The Meyers’ Singleton is explained in Item 4 of Scott Meyers’s Effective C++.
5 I know that the explicit handling of the copy and move assignment operators appears to be overkill, but this gives me the opportunity to remind you about the Rule of 5.
6 This behavior has changed in C++20, since the declaration of any constructor by the user is now enough to make a type nonaggregate.
7 To be precise and to avoid complaints, if the static local variable is zero or constant initialized, the initialization can happen before the function is entered. In our example, the variable is indeed created in the first pass.
8 In fact, a naive implementation of Singleton is creating lots of artificial dependencies itself; see “Guideline 38: Design Singletons for Change and Testability”.
9 Without going into detail, I argue that there are several more so-called “design patterns” that fall in the category of implementation patterns, such as the Monostate pattern, the Memento pattern, and the RAII idiom, which Wikipedia lists as design patterns. While this might make sense in languages other than C++, the intent of RAII is most certainly not to reduce dependencies but to automate cleanup and encapsulate responsibility.
10 Another such piece of advice is the CppCon 2020 talk by Peter Muldoon’s “Retiring the Singleton Pattern: Concrete Suggestions for What to Use Instead”, which provides many useful techniques for how to deal with Singletons in your codebase.
11 If a Singleton is used for anything else, you should be very suspicious and consider it a misuse of the Singleton pattern.
12 Michael Feathers, Working Effectively with Legacy Code.
13 The best summary of SIOF I’m aware of is given by Jonathan Müller in his accordingly named talk “Meeting C++ 2020”.
14 “Globals are bad, m’kay?” as stated by Guy Davidson and Kate Gregory in Beautiful C++: 30 Core Guidelines for Writing Clean, Safe, and Fast Code (Addison-Wesley).
15 The Monostate pattern, to my best knowledge, was first mentioned in the September issue of the 1996 C++ Report in the article “Monostate Classes: The Power of One” by Steve Ball and John Crawford (see Stanley B. Lippmann, ed., More C++ Gems (Cambridge University Press)). It is also described in Martin Reddy’s API Design for C++ (Morgan Kaufmann). Monostate, in contrast to Singleton, allows any number of instances of a type, but makes sure that there is only a single state for all instances. As such, the pattern should not be confused with std::monostate
, which is used as a well-behaved empty alternative in std::variant
.
16 Miško Hevery, “Root Cause of Singletons”, The Testability Explorer (blog), August 2008.
17 Ibid.
18 For an explanation about the different kinds of test doubles, see Martin Fowler’s article “Mocks Aren’t Stubs”. For examples of how to use these in C++, refer to Jeff Langr’s Modern C++ Programming with Test-Driven Development.
19 But I am sure you won’t be deterred from writing the tests anyway, despite it being difficult.
20 This is also one of the strong arguments in Robert C. Martin’s Clean Architecture.
21 For the design pattern experts, I should explicitly point out that the std::pmr::get_default_resource()
function itself fulfills the intent of another design pattern: the Facade design pattern. Unfortunately, I do not go into detail about Facade in this book.
22 The separation into a public
interface and a private
implementation is an example of the Template Method design pattern. Unfortunately, in this book I can’t go into detail about the many benefits of this design pattern.