Thread-Safe Singleton
Guaranteeing exactly-once construction of a singleton across concurrent threads using function-local statics, std::call_once, or atomic DCLP.
Thread-Safe Singletonsince C++11A singleton initialization strategy that guarantees exactly one instance is constructed regardless of how many threads concurrently invoke the accessor, relying on C++11's memory model rather than platform-specific primitives or ad-hoc locking.
Overview
Prior to C++11, the C++ standard said nothing about threads. "Thread-safe initialization" was entirely the programmer's problem, and every common approach had a crack in it. A raw static T* p = nullptr with a mutex around the first assignment is races-prone: the load of p before acquiring the lock is a data race in C++11's formal model and was exploitable on weakly-ordered CPUs even before that. The naive double-checked locking pattern (DCLP)—checking the pointer before and after locking—compiled to correct assembly on x86 by accident, but was undefined behavior on any architecture where stores could reorder, and compilers were free to "optimize" it into the broken form.
C++11 resolved this at two levels. It introduced a formal memory model with sequenced-before, happens-before, and synchronized-with relations. It also mandated in [stmt.dcl] that if multiple threads simultaneously enter the initialization of a function-local static variable, all but one block until initialization completes. That one rule makes the Meyers singleton correct without any user-level locking.
There are three credible approaches in modern C++:
- Function-local static (Meyers singleton) — zero locking on the fast path, simplest code.
std::call_once+std::once_flag— explicit, heap-compatible, retry-on-exception.- Double-Checked Locking with
std::atomic— heap-allocated, precise memory-ordering control.
Approach 1: Function-Local Static
#include <string_view> // C++17
class Logger {
public:
static Logger& instance() {
static Logger inst; // C++11: initialized exactly once, even under concurrency
return inst;
}
void log(std::string_view msg); // C++17
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
Logger(Logger&&) = delete;
Logger& operator=(Logger&&) = delete;
private:
Logger();
};The compiler inserts a hidden guard variable (typically a one-byte flag) and a platform fence. The fast path after initialization is a single load and a predicted branch — usually two or three cycles. Destruction is registered with atexit automatically, in reverse construction order relative to other statics.
This is the right default. Use it whenever the concrete type is known at compile time and you don't need retry semantics on a throwing constructor.
Lifetime caveat: If another static's destructor calls Logger::instance() after Logger has been destroyed, the behavior is undefined — the guard flag is not reset. If cross-singleton dependencies during teardown are a concern, see the heap-leak mitigation in Common Pitfalls.
Approach 2: std::call_once
std::call_once (C++11, <mutex>) provides once-initialization with a heap-allocated instance, which unlocks runtime-polymorphic singletons and explicit lifetime control:
#include <memory> // C++11
#include <mutex> // C++11
class ConnectionPool {
public:
static ConnectionPool& instance() {
std::call_once(init_flag_, &ConnectionPool::create);
return *instance_;
}
ConnectionPool(const ConnectionPool&) = delete;
ConnectionPool& operator=(const ConnectionPool&) = delete;
private:
static void create() {
instance_ = std::make_unique<ConnectionPool>(128); // C++14: make_unique
}
explicit ConnectionPool(int max_connections);
// C++17 inline static removes the need for out-of-class definitions
inline static std::unique_ptr<ConnectionPool> instance_;
inline static std::once_flag init_flag_;
};std::once_flag is a one-shot event. Once the callable passed to call_once returns normally, the flag is set and every subsequent call on the same flag becomes a no-op — no lock is acquired. The synchronization guarantee is strong: the end of the active (returning) call synchronizes-with the return from every passive call on the same flag, so all threads observe the constructed object's state without additional barriers.
Exception semantics differ from the Meyers approach. If the callable throws, the flag is not flipped. The next call to call_once with the same flag will retry initialization. This lets you recover from transient failures (e.g., a database pool that can't connect on the first attempt). The Meyers singleton offers no such retry — a throwing constructor during static initialization calls std::terminate.
Prefer call_once over the Meyers singleton when:
- The concrete type is determined at runtime (factory pattern, configuration-driven).
- You need to reset or replace the instance in tests.
- You want retry-on-exception during initialization.
Approach 3: Double-Checked Locking with std::atomic
DCLP is correct in C++11 when implemented with std::atomic and the appropriate memory orders. It is rarely the best choice today — reach for it only when profiling has identified call_once's internal bookkeeping as a bottleneck on a genuinely hot path.
#include <atomic> // C++11
#include <mutex> // C++11
class MetricsRegistry {
public:
static MetricsRegistry* instance() {
// Fast path: acquire-load pairs with the release-store after construction.
MetricsRegistry* p = instance_.load(std::memory_order_acquire);
if (!p) {
std::lock_guard<std::mutex> lock(mutex_); // C++11
// Re-read under the lock: another thread may have won the race.
p = instance_.load(std::memory_order_relaxed);
if (!p) {
p = new MetricsRegistry();
instance_.store(p, std::memory_order_release);
}
}
return p;
}
private:
MetricsRegistry() = default;
inline static std::atomic<MetricsRegistry*> instance_{nullptr}; // C++17
inline static std::mutex mutex_;
};The acquire-load / release-store pair is load-bearing. The release-store ensures that the write to instance_ cannot be observed by any thread before the constructor body has finished executing. The acquire-load ensures that any thread reading a non-null pointer also sees all memory writes performed before that release-store. Relaxing either order breaks the guarantee on ARM, POWER, or any other weakly-ordered architecture.
Why DCLP failed before C++11: Without a formal memory model, nothing prevented compilers from reordering the pointer store before the constructor body. A racing thread would read a non-null pointer and dereference a partially-constructed object. volatile was never the answer — it disables certain compiler optimizations but provides zero inter-thread ordering guarantees and is explicitly not a synchronization mechanism in the C++ standard.
Best Practices
Default to the Meyers singleton. It is correct by construction in C++11 and later, requires no auxiliary types, and has the lowest overhead after initialization. Its only real constraint is that the type must be known statically.
Delete all four special members. Delete the copy constructor, copy-assignment, move constructor, and move-assignment operators. Leaving move members implicitly available allows code to accidentally extract the singleton from its accessor.
Keep the constructor private. This is the invariant that enforces single-instance semantics at compile time, not at runtime.
Avoid singletons that depend on each other during teardown. Static destruction order is the reverse of construction order within a translation unit but unspecified across translation units. If A::~A() calls B::instance() and B has already been destroyed, the behavior is undefined.
Use std::call_once in test harnesses. In production code the flag is set once per process. In tests, construct a fresh once_flag and unique_ptr pair per test fixture using a reset mechanism rather than fighting the static lifetime.
Common Pitfalls
Assuming C++03 safety. The function-local static thread-safety guarantee is a C++11 language rule. Code compiled as C++03 — or with a compiler that predates the rule — can race on the guard variable. Audit any pre-C++11 codebase carefully.
Using volatile as a synchronization primitive. volatile prevents the compiler from caching a memory location in a register; it has no effect on CPU reordering and establishes no happens-before relationship. DCLP with volatile T* instead of std::atomic<T*> is a data race under the C++11 memory model.
Loading with memory_order_relaxed on the fast path of DCLP. The outer load must use memory_order_acquire. A relaxed load can be reordered by the CPU to occur before the null check passes, causing a thread to read an unconstructed object's fields.
Reusing a once_flag for independent initializations. A once_flag is a one-shot event that cannot be reset. Using the same flag for two separate initialization tasks silently skips the second one after the first succeeds.
The Phoenix problem. If destruction-time code (e.g., another static's destructor or a thread still running during shutdown) calls instance() after the Meyers singleton has been destroyed, the guard flag is not reset and the returned reference is to a destroyed object. A common mitigation for singletons with complex teardown dependencies is to new the instance and never delete it — the OS reclaims the memory at process exit, and the object is never formally destroyed, preventing use-after-destroy. This is a deliberate and documented tradeoff, not a leak in the pejorative sense.
constinit and the Static Initialization Order Fiasco (C++20)
The three approaches above all concern when initialization happens at runtime. constinit (C++20, <no header>) addresses a different problem: ensuring that a static singleton's initial state is computed at compile time, before main() runs and before any dynamic initialization of other translation units can race against it.
// C++20
constexpr int compute_initial_capacity() { return 128; }
class FixedRegistry {
public:
static FixedRegistry& instance() {
static FixedRegistry inst; // initialized once, thread-safe per C++11
return inst;
}
int capacity() const { return capacity_; }
private:
explicit FixedRegistry() : capacity_(initial_capacity_) {
}
// constinit: guaranteed to be constant-initialized before any
// dynamic initialization — no static initialization order fiasco.
inline static constinit int initial_capacity_ = compute_initial_capacity();
int capacity_;
};constinit asserts that the variable undergoes constant initialization (zero-initialization followed by constant-expression initialization) rather than dynamic initialization. If the initializer cannot be evaluated at compile time, the program is ill-formed — a hard compile error rather than a runtime surprise. The variable itself is not const; you can modify it after program start.
The problem constinit solves. Across translation units, the order of dynamic initialization of namespace-scope statics is unspecified. If a static in TU B depends on a static in TU A being initialized first, and TU B's objects happen to initialize first, the program reads zero-initialized memory — the infamous "static initialization order fiasco." Marking the depended-upon variable constinit eliminates the race: it is guaranteed to be in its final state before any dynamic initialization begins anywhere.
Key distinctions from constexpr:
| Property | constexpr | constinit |
|---|---|---|
| Requires compile-time init | Yes | Yes |
Variable is const (immutable) | Yes | No |
| Can be modified at runtime | No | Yes |
| Mandates constant destruction | Yes | No |
Works with std::shared_ptr<T> static | No | Yes (if constexpr constructor) |
constinit in the context of singletons. The Meyers singleton already avoids the fiasco for the instance itself (because function-local statics are initialized lazily on first call). constinit is most useful for supporting data — configuration tables, capacity constants, or registry maps — that must be ready before dynamic initialization of other statics that might call into your singleton during their own construction.
// C++20: a compile-time-initialized lookup table that singletons can safely
// read during their own dynamic initialization.
constinit inline std::array<int, 4> priority_table = {10, 20, 30, 40};Approach Comparison
The table below summarises the practical tradeoffs across all thread-safe initialization strategies.
| Meyers Singleton | std::call_once | Atomic DCLP | constinit static | |
|---|---|---|---|---|
| C++ standard | C++11 | C++11 | C++11 | C++20 |
| Thread safety | Guaranteed by [stmt.dcl] | Guaranteed by once_flag | Requires correct memory_order | N/A (no dynamic init) |
| Heap allocated | No | Yes | Yes | No |
| Retry on exception | No (std::terminate) | Yes | No | No |
| Testability | Hard (static lifetime) | Moderate (can reset unique_ptr) | Moderate (can reset pointer) | Hard (compile-time constant) |
| Code complexity | Minimal | Low | High | Minimal (annotation only) |
| Init timing | First call | First call_once | First call | Compile time |
| Cross-TU init order safe | No (lazily avoids it) | No (lazily avoids it) | No (lazily avoids it) | Yes (compile-time guarantee) |
| Runtime polymorphism | No | Yes | Yes | No |
Reading the table: "Testability" refers to how easily a unit test can replace or reset the singleton instance. Lazy patterns (Meyers, call_once, DCLP) all defer construction to first use, which means a test running after another test sees the already-constructed instance and cannot reset it without extra machinery. constinit globals present the same fixed-lifetime problem.
Testability and the Dependency Injection Alternative
Thread-safe initialization solves the concurrency problem but not the testability problem. A singleton's static lifetime spans the entire process. When one test calls Logger::instance(), every subsequent test in the same process sees the same Logger — its internal state (counters, buffers, recorded messages) persists across test boundaries. Tests become order-dependent; a failure in one test can corrupt state observed by another.
The root cause is that singletons are hidden dependencies. A class method that calls DatabaseConnection::instance() internally looks simple from the outside, but it silently pulls in the global database, the network layer, and everything else the database connection touches. You cannot substitute a mock, a stub, or an in-memory replacement without modifying source code.
The dependency injection alternative. Instead of a singleton, pass the shared resource explicitly:
// Before: hidden dependency on a singleton
class OrderProcessor {
public:
void process(int id) {
DatabaseConnection::instance().update(id); // untestable
}
};
// After: explicit dependency, injected at construction
class IDatabase {
public:
virtual ~IDatabase() = default;
virtual void update(int id) = 0;
};
class OrderProcessor {
public:
explicit OrderProcessor(IDatabase& db) : db_(db) {
}
void process(int id) { db_.update(id); }
private:
IDatabase& db_;
};
// Production
DatabaseConnection real_db;
OrderProcessor proc(real_db); // real connection
// Test
MockDatabase mock_db;
OrderProcessor proc(mock_db); // no real I/O, deterministicThe composition root — typically main() or a top-level application object — creates the shared instance once and passes it down. You get single-instance semantics in production without the global state that breaks tests. C++20 concepts can replace virtual dispatch entirely if you prefer zero-overhead injection:
template<typename DB>
requires requires(DB& db, int id) { db.update(id); }
class OrderProcessor {
public:
explicit OrderProcessor(DB& db) : db_(db) {
}
void process(int id) { db_.update(id); }
private:
DB& db_;
};When a singleton is still the right answer. Singletons are most defensible for resources that are truly global in nature (process-wide signal handlers, OS-level hardware interfaces) and whose state does not affect the outcome of unit tests. Even then, prefer a singleton behind an abstract interface so tests can substitute a no-op implementation. If a singleton's state does affect test outcomes — for example, a metrics counter that accumulates across tests — it must either be reset between tests or replaced entirely with dependency injection.
Singleton with custom deleter
Sometimes you need explicit cleanup before process exit, not the default atexit destruction. A std::unique_ptr with a custom deleter gives you full control over when and how the instance is torn down, without giving up RAII.
#include <memory> // C++11
#include <mutex> // C++11
struct ResourceHandle {
void close(); // releases OS handle, flushes buffers, etc.
};
struct ResourceDeleter {
void operator()(ResourceHandle* p) const {
if (p) { p->close(); delete p; }
}
};
class ResourceManager {
public:
static ResourceManager& instance() {
std::call_once(flag_, [] {
handle_.reset(new ResourceHandle{});
});
return *handle_;
}
ResourceManager(const ResourceManager&) = delete;
ResourceManager& operator=(const ResourceManager&) = delete;
private:
ResourceManager() = default;
// C++17 inline static: no out-of-class definition required
inline static std::unique_ptr<ResourceManager, ResourceDeleter> handle_;
inline static std::once_flag flag_;
};Custom deleters compose with std::shared_ptr too. If multiple subsystems hold shared ownership of the singleton — a rare but legitimate pattern in plugin architectures — swap unique_ptr for shared_ptr<T> with a custom deleter and keep the call_once guard on construction.
Parameterized singleton
A parameterized singleton accepts configuration on first construction and returns the same configured instance on every subsequent call. The tricky invariant: parameters must be supplied exactly once, before any call that doesn't supply them.
#include <mutex> // C++11
#include <string> // C++11
#include <stdexcept>
class AppConfig {
public:
struct Params {
std::string config_path;
int max_threads = 4;
};
// First call must provide params; subsequent calls ignore them.
static AppConfig& instance(const Params* p = nullptr) {
std::call_once(flag_, [&] {
if (!p) throw std::logic_error("AppConfig: first call must supply Params");
inst_.reset(new AppConfig(*p));
});
return *inst_;
}
const std::string& config_path() const { return params_.config_path; }
int max_threads() const { return params_.max_threads; }
AppConfig(const AppConfig&) = delete;
AppConfig& operator=(const AppConfig&) = delete;
private:
explicit AppConfig(const Params& p) : params_(p) {}
Params params_;
inline static std::unique_ptr<AppConfig> inst_; // C++17
inline static std::once_flag flag_;
};
// In main():
// AppConfig::Params p{ .config_path = "/etc/app.conf", .max_threads = 8 }; // C++20
// AppConfig::instance(&p); // constructs
// AppConfig::instance(); // retrieves — Params* ignoredThe call_once exception retry property is load-bearing here. If AppConfig's constructor throws (e.g., the config file is missing), the once_flag is left unset and the next call to instance() retries — a cleaner failure mode than std::terminate.
Singleton lifetime and destruction order
Destruction order of static-duration objects is the reverse of construction order within a translation unit, but is unspecified across translation units. This is the core of the Static Initialization Order Fiasco applied to teardown.
// C++11 — illustrating the "phoenix singleton" guard
class Logger {
public:
static Logger& instance() {
static Logger inst;
// Mark ourselves as alive whenever instance() is entered.
// This runs before the object is returned, so any destructor-time
// call to instance() sees the flag while inst may be destroyed.
if (destroyed_) {
// Re-placement new into the already-allocated static storage
// resets state; the guard flag is not re-armed, so this only
// works when the caller doesn't require a fully valid object.
// Better: heap-leak pattern below.
}
return inst;
}
private:
Logger() { destroyed_ = false; }
~Logger() { destroyed_ = true; }
inline static bool destroyed_ = false; // C++17
};The simplest safe answer is the heap-leak pattern. Allocate the singleton with new and never register a destructor. The OS reclaims the memory at process exit; the object is never formally destroyed, so no post-destruction access is possible. This is a deliberate design choice, not a defect, and is appropriate when the singleton's destructor performs no externally-visible side effects that must complete before exit.
// C++11: heap-leak singleton — safe against use-after-destroy
class Registry {
public:
static Registry& instance() {
static Registry* p = new Registry(); // never deleted
return *p;
}
private:
Registry() = default;
};For resources that must be flushed or closed (file handles, network sockets), prefer an explicit shutdown() call in main() over relying on destructor order.
Singleton in shared libraries
A singleton naively placed in a shared library produces one instance per library load, not one per process — violating the fundamental invariant. Each DSO (.so / .dll) that defines instance() gets its own copy of the static guard variable and object.
// C++11 — marking the accessor for symbol visibility on Linux/macOS
// In the shared library's public header:
class __attribute__((visibility("default"))) PluginRegistry {
public:
static PluginRegistry& instance();
PluginRegistry(const PluginRegistry&) = delete;
PluginRegistry& operator=(const PluginRegistry&) = delete;
private:
PluginRegistry() = default;
};
// In exactly one translation unit compiled into the shared library:
PluginRegistry& PluginRegistry::instance() {
static PluginRegistry inst; // C++11: thread-safe init
return inst;
}On Linux, compile the shared library with -fvisibility=hidden and annotate only the instance() function as default. This ensures all consumers of the DSO resolve to the same symbol, sharing the single guard variable. On Windows with MSVC, use __declspec(dllexport) / __declspec(dllimport) via a macro in the header. When multiple plugins are loaded at runtime, each should explicitly link against the shared library rather than statically embedding it, or they will each host a separate singleton instance.
Thread-local singleton
A thread-local singleton gives each thread its own instance, initialized on first use within that thread — eliminating contention entirely at the cost of per-thread state. Use this when the resource itself carries thread-specific context (e.g., a per-thread allocator, scratch buffer, or PRNG state).
#include <memory> // C++11
class ThreadArena {
public:
// C++11: thread_local + function-local static =
// one instance per thread, initialized on first call in that thread.
static ThreadArena& instance() {
thread_local ThreadArena arena; // C++11
return arena;
}
void* allocate(std::size_t n);
void reset();
ThreadArena(const ThreadArena&) = delete;
ThreadArena& operator=(const ThreadArena&) = delete;
private:
ThreadArena() = default;
~ThreadArena() = default;
alignas(64) char buffer_[1 << 20]; // 1 MiB scratch buffer, cache-line aligned
std::size_t used_ = 0;
};Destruction happens when the thread exits, not when the process exits. Each thread's arena is torn down as its stack unwinds, which is usually what you want. Be careful with thread pools: a pooled thread's thread_local objects persist for the life of the thread, not the life of a single task. Call instance().reset() explicitly between tasks if you need a clean slate.
// C++17: heap-allocated thread-local for types that are not
// trivially destructible and need precise lifetime.
class ThreadLogger {
public:
static ThreadLogger& instance() {
thread_local auto inst = std::make_unique<ThreadLogger>(); // C++14
return *inst;
}
private:
ThreadLogger() = default;
};Testing singletons: resettable pattern
The fundamental testing problem is that a singleton's static lifetime spans the entire test process. The resettable pattern keeps production code clean while giving tests a controlled escape hatch.
#include <memory> // C++11
#include <mutex> // C++11
class MetricsStore {
public:
static MetricsStore& instance() {
std::call_once(flag_, &MetricsStore::create);
return *inst_;
}
void record(int value) { /* ... */ }
int total() const { return total_; }
MetricsStore(const MetricsStore&) = delete;
MetricsStore& operator=(const MetricsStore&) = delete;
#ifdef UNIT_TESTING
// Only compiled in during test builds. Never call from production code.
static void reset_for_testing() {
inst_.reset();
flag_ = std::once_flag{}; // re-arm: next call_once will reinitialize
}
static void inject_for_testing(std::unique_ptr<MetricsStore> p) {
inst_ = std::move(p);
// Mark the flag as "done" so future instance() calls skip construction.
std::call_once(flag_, [] {});
}
#endif
private:
static void create() { inst_ = std::make_unique<MetricsStore>(); }
MetricsStore() = default;
int total_ = 0;
inline static std::unique_ptr<MetricsStore> inst_; // C++17
inline static std::once_flag flag_;
};The inject_for_testing overload enables full mock substitution. Create a subclass or a completely separate MetricsStore-shaped object, wrap it in a unique_ptr, and inject it before the test runs. The call_once sentinel call prevents the real constructor from firing on the first instance() call inside the system under test.
// In a Google Test fixture:
class MetricsTest : public ::testing::Test {
protected:
void SetUp() override {
#ifdef UNIT_TESTING
MetricsStore::reset_for_testing();
#endif
}
};For codebases where #ifdef in production headers is unacceptable, prefer the dependency injection pattern shown in "Testability and the Dependency Injection Alternative" above. Reserve the resettable singleton pattern for legacy code where refactoring to DI is not yet feasible.
See Also
reference/idioms/singleton— the baseline pattern without thread-safety considerationsreference/idioms/lazy-initialization— deferred construction in non-singleton contexts