3 Tips for Using Singletons in C++

Encapsulation logic, deferred and dynamic initialization, and ordered dynamic initialization

Josh Weinstein
Better Programming

--

Photo by Federico Beccari on Unsplash

Singletons serve a wide variety of purposes in almost any programming language. In C++, singletons allow encapsulating logical that exists globally within a program. Instead of passing around a heap allocated object across function calls, a singleton’s unique instance can be accessed anywhere. However, it’s important to be careful of initialization order problems when singletons are used. Singletons may also be constructed at different points at runtime, depending on the desired behavior. This article will detail each of these tips and how to best use singletons.

What is a Singleton?

First, let’s define what exactly a singleton is. A singleton is an object with only a single instance that exists within a program. Typically, it cannot be destructed, and lives until the end of the program once constructed.

Singletons are not directly supported in the C++ language but must be implemented using primitive guarantees of initialization behavior.

Let’s take a simple struct, foobar , and make it into a singleton. Then, the example will demonstrate:

Here, foobar is a singleton struct with the member value . The instance() method of foobar returns the singular instance of the struct. The static foobar base; inside the instance() method uses deferred initialization.

As of C++11, the standard guarantees that static objects within functions only get initialized the first time the function is called, not before main() gets called, like most other static storage objects do.

Not only that, but there’s also a guarantee that the initialization only happens once. But to double check on that claim, let’s test it. Take 2 threads, make them retrieve the singleton instance a bunch of times, and confirm the constructor only runs once for a single thread.

This test, when run, should print something like:

Main Thread:  0x105bf3dc0Constructed by: 0x700007865000

Where the main thread id is different than the thread that constructs the singleton. This shows that the singleton construction is in fact deferred to when the child threads first call instance() .

Encapsulating Logic

Singletons allow grouping and encapsulation of global access patterns that would be very difficult to do without them.

One example of a global access pattern is a shared queue, where multiple parts of a program may enqueue or dequeue an object, such as a job. Ideally, such a queue should be thread-safe, and use a mutex. Using the singleton pattern explained before, here’s what the job queue looks like

First of all, this singleton uses r-value references, as opposed to l-value references. Jobs are moved onto the queue as opposed to being copied onto the queue.

The behavior of movement not only helps avoid unnecessary copying, it shapes the idea that a job should only be on the queue or not on the queue, never in both states at the same time.

Anytime, anywhere that JobQueue::instance()->enqueue(Job&&) is called, the global size of the job queue increases. Any subsequent call to JobQueue::instance()->dequeue() always reflects the last state from the enqueue call. The initialization of the queue and its thread safety is totally encapsulated within the singleton.

Deferred and Dynamic Initialization

In terms of static data variables and members, there are two main types of initialization. Deferred initialization is what we described earlier, that a given variable will be initialized the first time it is accessed.

Dynamic initialization is vastly different as it is unordered. This means that the point in time in which the variable is initialized is undetermined. All that one can know is that it will be initialized before main() is called.

One way to think of that is a dynamically initialized boolean would be indeterminate, it’s unknown if it’s been initialized to false , or yet to be initialized. Here’s what the C++ reference mentions:

Unordered dynamic initialization, which applies only to (static/thread-local) class template static data members and variable templates (since C++14) that aren’t explicitly specialized. Initialization of such static variables is indeterminately sequenced with respect to all other dynamic initialization except if the program starts a thread before a variable is initialized, in which case its initialization is unsequenced (since C++17). Initialization of such thread-local variables is unsequenced with respect to all other dynamic initialization

The biggest problem with dynamic initialization is the lack of order presents the risk of using an uninitialized variable. This happens when one dynamically initialized static variable depends on some other dynamically initialized static variable.

Since both will be constructed at some point before main() , there’s no guarantee that the order the programmer may intend for them to be constructed in would in fact be the order used. Here’s an example of a design pattern that’s at risk for that:

In the above, each Member instance depends on the availability of regr_manager being constructed and initialized. Since there’s no guarantee of such order, this design could encounter a static initialization ordering bug.

Thus, the solution here would be to convert the use of Registrar to a singleton that’s deferred initialized. This would ensure that for any Member , there’s always the Registrar instance that’s available.

Ordered Dynamic Initialization

There’s one important exception toward the definition of dynamic initialization. You may have noticed that compiling and running the program with Registrar and Member objects doesn’t run into problems.

That exception takes place when the relationship between static data members are contained within a single translation unit during the compilation process. If that condition is true, then the dynamic initialization does take place, but only in the order in which those variables appear syntactically. Specifically:

Ordered dynamic initialization, which applies to all other non-local variables: within a single translation unit, initialization of these variables is always sequenced in exact order their definitions appear in the source code. Initialization of static variables in different translation units is indeterminately sequenced. Initialization of thread-local variables in different translation units is unsequenced.

Although the above is true, it’s a very unreliable and error prone design choice. That’s because the build steps of a C++ program are external to the language itself. Looking at preprocessor statements like #include does not indicate whether or not a variable is in a separate translation unit or not.

Some build tools like unity builds paste many .cpp files into a single file before compilation. Regardless, the point is one should not create a dependency on a particular build arrangement of files in a project that isn’t visible from the language itself.

Static Ordering Initialization Fiasco

A potential problem with using non-local static initialization, is the order in which those variables are initialized is undefined. If two or more non-local statics reference each other , there’s a possibility they won’t be initialized in the order the program intends them to be. This fiasco is especially true when using objects with static constructors.

Consider the case where one object depends on a static instance of another in its constructor, where you first have a base, container like class.

struct Base {
Link* list;
void addLink(Link* ptr) {
ptr->next = list;
list = ptr;
}
};
static Base the_base;

and a node type class, that adds itself to the base list upon construction.

struct Link {  Link(int num) {
val = num;
the_base.addLink(this);
}
int val;
Link* next;
};
static Link a(3);

There’s no guarantee the_base is initialized before the first Link object is.

Orders of Destruction

Similarly to no defined order to initialization, there’s also no defined order to the destruction of static objects. Non-local static objects that are also statically allocated have exit handlers that are called upon exit. This can be true for common types like std::unique_ptr or std::shared_ptr . Having a static std::shared_ptr<type> at the non local scope leads to a situation where that object can be destructed before it’s done being used.

Dynamic Allocation with Deferred Initialization

A solution to both the problems of the ordering of initialization and destruction for static objects is pairing dynamic allocation with deferred initialization. What this means is, we want to use new to allocate the object, but do it in the function level scope so it will be initialized in a thread safe manner the first time that function is called. When this happens, the static object will instead be a pointer to heap memory rather than allocated statically, and thus, will not undergo any automatic destruction.

--

--

I’m an engineer on a mission to write the fastest software in the world.