Hello! Welcome to the first lesson in our module on C++ Design Patterns.
In the previous modules, we've built a strong foundation in C++ concurrency, exploring the low-level tools the language provides: std::mutex, std::atomic, std::condition_variable, and the happens-before memory model. We concluded by comparing lock-based and lock-free approaches, understanding that the best performance often comes from algorithmic design that minimizes contention, rather than just from using a specific primitive.
Now, we're going to shift our focus upward from primitives to patterns. Design patterns are high-level, reusable solutions to commonly occurring problems in software design. They provide a shared vocabulary and a blueprint for structuring code. Your goal to build an exceptional portfolio requires not just mastering low-level mechanics but also demonstrating architectural competence, and patterns are a key part of that.
Today, we'll start with one of the most well-known (and sometimes controversial) creational patterns: the Singleton. Its purpose is to ensure that a class has only one instance and to provide a single, global point of access to it. In systems programming, this is frequently used for managing resources like a global logger, a configuration service, or an interface to a specific hardware device.
Our primary learning outcome is to implement a thread-safe Singleton pattern using std::call_once to prevent race conditions during initialization. We'll see how the concurrency concepts we've just learned are applied to solve a real-world architectural problem.
1. The Singleton Pattern: A Single Point of Access
The core idea of the Singleton is straightforward. To enforce that only one instance of a class can exist, we must take control of its creation. We do this by:
- Making the constructor
private(orprotected). - Deleting the copy constructor and copy assignment operator to prevent duplicates.
- Providing a
public staticmethod (commonly namedgetInstance()) that creates the instance if it doesn't exist and returns a reference or pointer to it.
Here is a basic, single-threaded implementation:
class Logger {
public:
// 3. Public static method to get the instance
static Logger* getInstance() {
if (instance == nullptr) {
instance = new Logger();
}
return instance;
}
void log(const std::string& message) {
// Implementation for logging...
std::cout << "LOG: " << message << std::endl;
}
// 2. Delete copy operations
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
private:
// 1. Private constructor
Logger() = default;
// Static pointer to the single instance
static Logger* instance;
};
// Initialize the static member
Logger* Logger::instance = nullptr;
// Usage:
// Logger::getInstance()->log("Application started.");
This works perfectly fine in a single-threaded program. But as you know from our previous discussions, the moment we introduce multiple threads, this code becomes dangerously broken.
2. The Problem: A Race to Initialize
Consider what happens when two threads call Logger::getInstance() at roughly the same time:
- Thread A calls
getInstance(), seesinstanceisnullptr, and passes theifcheck. - The OS preempts Thread A and schedules Thread B.
- Thread B calls
getInstance(), also seesinstanceisnullptr, and passes theifcheck. - Thread B executes
new Logger(), creating an object, and assigns its address toinstance. - The OS schedules Thread A again. It resumes from where it left off, inside the
ifblock. - Thread A executes
new Logger(), creating a second object, and assigns its address toinstance, overwriting the pointer from Thread B.
This is a classic data race. The result is a memory leak (the object created by Thread B is lost forever) and a violation of the Singleton's core guarantee.
3. Attempt #1: The Naive Lock
The most obvious way to fix the race condition is to use a mutex. In the article "Thread-Safe Initialization of a Singleton" from Modernes C++, you can see a direct implementation of this idea.
Thread-Safe Initialization of a Singleton – MC++ BLOG
Let's examine a straightforward but flawed approach using a simple lock. This article provides a clear code example.
Read the section titled 'Lock' and study the singletonLock.cpp code. Notice how std::lock_guard is used inside getInstance() to protect the check and creation.
The code you reviewed looks something like this:
// Simplified from the article
class MySingleton {
public:
static MySingleton& getInstance() {
std::lock_guard<std::mutex> myLock(myMutex); // Lock every time
if (!instance) {
instance = new MySingleton();
}
return *instance;
}
private:
// ... private constructor, deleted copy ops ...
static MySingleton* instance;
static std::mutex myMutex;
};
This is correct and thread-safe. However, it's inefficient. After the Singleton is initialized, the if (!instance) check will always be false. Yet, every single call to getInstance() must acquire and release the mutex. This introduces unnecessary contention and overhead, especially in a high-throughput system. We're paying the full cost of a lock (which can involve system calls) on every call, even though we only need synchronization for the very first one.
4. Attempt #2: The Modern C++ Solution with std::call_once
To solve the performance problem of the naive lock, we need a mechanism that performs the expensive synchronization only once. This is precisely what std::call_once is designed for. It guarantees that a given callable (like a function or lambda) will be executed exactly once, even if called concurrently from multiple threads.
std::call_once works with an associated std::once_flag.
Thread-Safe Initialization of a Singleton – MC++ BLOG
The same article from Modernes C++ demonstrates the canonical way to implement a thread-safe Singleton using std::call_once.
Please read the section 'The function std::call_once and the flag std::once_flag' and study the singletonCallOnce.cpp code. This is the central learning objective for this lesson.
Let's break down the pattern you just saw:
#include <mutex> // for std::call_once, std::once_flag
class MySingleton {
public:
static MySingleton& getInstance() {
// Guarantees initSingleton() is called exactly once
std::call_once(initInstanceFlag, &MySingleton::initSingleton);
return *instance;
}
private:
MySingleton() = default;
~MySingleton() = default;
MySingleton(const MySingleton&) = delete;
MySingleton& operator=(const MySingleton&) = delete;
// The function to be called once
static void initSingleton() {
instance = new MySingleton();
}
static MySingleton* instance;
static std::once_flag initInstanceFlag; // The flag for call_once
};
// Static member definitions
MySingleton* MySingleton::instance = nullptr;
std::once_flag MySingleton::initInstanceFlag;
How it works:
- When
getInstance()is called, it invokesstd::call_once. - The first thread to reach this line will execute the
initSingletonfunction and "flip" theinitInstanceFlag. - Any other threads that arrive while the first thread is busy in
initSingletonwill block until it completes. - Once
initSingletonhas finished, all blocking threads are unblocked, and all subsequent calls togetInstance()will see that the flag has been flipped and will return immediately without executinginitSingletonor blocking.
This gives us the best of both worlds: thread safety and performance. The fast path (after initialization) involves only a cheap check on the state of the once_flag.
5. Under the Hood: How is std::call_once Implemented?
Your interest is in the "how," so let's dig deeper. How can the standard library implement std::call_once? It's essentially a well-implemented version of the Double-Checked Locking Pattern (DCLP).
Understanding std::call_once() in C++
This article provides a simplified, conceptual implementation of std::call_once and std::once_flag that reveals the mechanics.
Read the section titled 'Implementation'. Focus on the structure of the custom once_flag class (containing a mutex and an atomic bool) and the logic within the call_once function. This is the key to understanding the pattern.
As the article explains, a std::once_flag can be thought of as a struct containing a std::mutex and a std::atomic<bool>. The call_once logic then follows the DCLP structure:
// Conceptual implementation based on the article
template<typename Callable, typename... Args>
void call_once(once_flag& flag, Callable&& f, Args&&... args) {
// 1. First check: Fast, non-locking atomic read.
// This is the path taken for all calls after initialization.
if (flag.has_run.load(std::memory_order_acquire)) {
return;
}
// 2. Slow path: acquire a lock
std::lock_guard<std::mutex> lock(flag.m);
// 3. Second check: Another thread might have finished
// initialization while we were waiting for the lock.
if (flag.has_run.load(std::memory_order_acquire)) {
return;
}
// 4. Execute the callable and set the flag.
// The memory order ensures this write is visible before the lock is released.
f(std::forward<Args>(args)...);
flag.has_run.store(true, std::memory_order_release);
}
(Note: Real-world implementations are more complex, often using OS-specific primitives like futexes for better performance, but this captures the essential logic.)
This implementation beautifully illustrates the performance characteristics: the vast majority of calls hit the fast path in step 1. Only the very first few threads to arrive will enter the slow, locking path in step 2. The double-check in step 3 prevents redundant work. Also, standard implementations of std::call_once are exception-safe: if f() throws, the flag is not set, allowing another thread to attempt initialization.
Test your understanding!
In the conceptual call_once implementation above, why is the second check (if (flag.has_run)) inside the lock absolutely critical? What would happen if it were removed?
Show answer
If the second check were removed, we would reintroduce the original race condition in a slightly different form. Multiple threads could pass the first check, and then queue up at the mutex. As each thread acquires the lock one by one, it would blindly execute the function f() without re-validating the condition. This would lead to multiple initializations, defeating the entire purpose of the pattern. The second check ensures that only the very first thread to acquire the lock actually performs the initialization.
6. The Ultimate Simplification: The "Meyers Singleton"
Since C++11, there is an even simpler way to write a thread-safe Singleton that relies on language guarantees. It's often called the "Meyers Singleton," after Scott Meyers who popularized it.
Thread-Safe Initialization of a Singleton – MC++ BLOG
Let's go back to the Modernes C++ article to see the simplest and often best approach.
Read the section 'Meyers Singleton' and examine the singletonMeyers.cpp code. The elegance of this solution lies in its simplicity.
The entire implementation boils down to this:
class MySingleton {
public:
static MySingleton& getInstance() {
// The static variable is only initialized once, on first access.
// C++11 guarantees this initialization is thread-safe.
static MySingleton instance;
return instance;
}
private:
MySingleton() = default;
~MySingleton() = default;
MySingleton(const MySingleton&) = delete;
MySingleton& operator=(const MySingleton&) = delete;
};
That's it. The C++11 standard guarantees that the initialization of a function-local static variable is performed only once. If multiple threads attempt to call getInstance() concurrently, the compiler and runtime system ensure that only one thread will execute the initialization of instance, and all other threads will wait.
Under the hood, the compiler generates code that is functionally equivalent to using std::call_once or platform-specific DCLP mechanisms. On Linux with GCC/Clang, this typically involves the compiler emitting calls to guard functions in the C++ ABI, like __cxa_guard_acquire and __cxa_guard_release, which use a hidden flag variable to manage the thread-safe initialization. You get all the safety and performance without any of the boilerplate.
Conclusion
In this lesson, we explored the Singleton pattern and the critical challenge of making it thread-safe. We saw how a naive implementation fails and how simple locking is inefficient.
Key Takeaways:
- The Singleton pattern ensures a class has only one instance, which is crucial for managing global resources.
- Lazy initialization of a Singleton in a multithreaded environment is prone to race conditions.
std::call_onceis the standard, robust, and efficient C++ primitive for "initialize-once" logic. It is essentially a library-provided, correct implementation of the Double-Checked Locking Pattern.- The Meyers Singleton (using a function-local static variable) is the simplest and often preferred method in modern C++. It relies on language guarantees for thread-safe initialization, effectively hiding the complexity of
std::call_oncefrom the developer.
You have now applied your knowledge of concurrency to a common architectural pattern, moving up the stack from raw primitives to reusable design solutions.
In our next lesson, we will explore the Factory Method pattern. While the Singleton is about restricting object creation to a single instance, the Factory Method is about delegating the responsibility of which concrete class to create to subclasses, giving your programs more flexibility.