Singleton Pattern
Thread-safe Singleton in C++ — Meyers singleton, static local variable, shared-library pitfalls, destruction ordering, and when dependency injection wins.
Singletonsince C++11A creational pattern that restricts a class to exactly one instance and exposes a single global access point; since C++11, the standard implementation uses a function-local static variable whose initialization the language guarantees to occur exactly once, thread-safely, per §6.7.4 [stmt.dcl].
Overview
The Singleton pattern solves two distinct problems: ensuring only one instance of a class exists, and providing a controlled access point to it. Before C++11, both guarantees required manual synchronization. The C++11 memory model changed this: function-local static variables are now initialized exactly once, and if multiple threads race to trigger that initialization, all but one block until it completes. The compiler synthesizes the necessary barrier — you write nothing extra.
The Meyers singleton (named for Scott Meyers) exploits this guarantee directly. It is zero-overhead compared to hand-rolled double-checked locking and is the correct default in every modern C++ codebase.
Syntax
class Config {
public:
static Config& instance() {
static Config inst; // C++11: initialized exactly once, thread-safely
return inst;
}
Config(const Config&) = delete; // C++11
Config& operator=(const Config&) = delete; // C++11
Config(Config&&) = delete; // C++11
Config& operator=(Config&&) = delete; // C++11
private:
Config() = default; // C++11
};Delete all four special members. Deleting only the copy constructor leaves move-construction available — a caller could move out of the static, creating a second logical instance.
Examples
Logger with Internal Synchronization
The static local handles initialization; an internal mutex serializes the actual work:
#include <mutex>
#include <string_view>
class Logger {
public:
static Logger& instance() {
static Logger inst; // C++11: thread-safe initialization
return inst;
}
void log(std::string_view msg) {
std::lock_guard lk(mutex_); // C++17: CTAD for lock_guard
// write to sink...
}
Logger(const Logger&) = delete; // C++11
Logger& operator=(const Logger&) = delete;
private:
Logger() = default;
std::mutex mutex_;
};Resettable Singleton for Testing
When you need to tear down and recreate the singleton between test cases, use an inline static unique_ptr (C++17 inline for the static data member) and expose an explicit reset():
#include <any> // C++17
#include <memory>
#include <stdexcept>
#include <string>
#include <unordered_map>
class ServiceRegistry {
public:
static ServiceRegistry& instance() {
if (!inst_)
inst_.reset(new ServiceRegistry); // C++11
return *inst_;
}
static void reset() noexcept { inst_.reset(); } // noexcept: C++11
void register_service(std::string name, std::any svc) { // std::any: C++17
registry_.emplace(std::move(name), std::move(svc));
}
template<typename T>
T& get(const std::string& name) {
return std::any_cast<T&>(registry_.at(name));
}
private:
ServiceRegistry() = default;
inline static std::unique_ptr<ServiceRegistry> inst_; // inline static: C++17
std::unordered_map<std::string, std::any> registry_; // std::any: C++17
};This variant is not thread-safe on the construction path. The check-then-set on inst_ is a data race under concurrent access. Use it only when construction is single-threaded (e.g., test setup) or when you intentionally control the lifetime. For concurrent construction, fall back to the static-local form.
std::call_once Alternative (C++11)
std::call_once provides the same one-time initialization guarantee with more explicit control over when and how it triggers:
#include <memory>
#include <mutex> // std::once_flag, std::call_once: C++11
class HardwareDevice {
public:
static HardwareDevice& instance() {
std::call_once(flag_, [] { inst_.reset(new HardwareDevice); }); // C++11
return *inst_;
}
private:
HardwareDevice(); // opens platform handle
~HardwareDevice(); // releases platform handle
inline static std::once_flag flag_; // C++17 inline
inline static std::unique_ptr<HardwareDevice> inst_;
};Prefer the Meyers singleton for simplicity. Reach for call_once when you need to pass arguments to the factory lambda or want to delay registration until runtime.
Best Practices
Prefer dependency injection over hidden singleton calls. Pass the singleton to consumers instead of having them reach for instance() internally. This makes dependencies visible and keeps code testable without touching the singleton implementation:
// Coupled — Logger::instance() is a hidden dependency, untestable in isolation
void process(const Request& req) {
Logger::instance().log("processing");
Database::instance().execute("...");
}
// Decoupled — dependencies are explicit parameters
void process(const Request& req, Logger& log, Database& db) {
log.log("processing");
db.execute("...");
}
// Production: pass the real singletons
process(req, Logger::instance(), Database::instance());
// Test: pass mocks without modifying any singleton
MockLogger mock_log;
MockDatabase mock_db;
process(req, mock_log, mock_db);Use singleton only when there must be exactly one. Hardware devices, application-wide configuration loaded once, metrics sinks — these are genuine singletons. Anything that might need two instances in a future deployment (database pools, caches, feature-flag clients) should be injected from the start.
Localize the singleton in a shared library when linking into multiple DSOs. See the pitfall below.
Common Pitfalls
Static Initialization Order Fiasco (SIOF)
Non-local static objects (globals) across translation units initialize in an unspecified order. A global that tries to use another global may run before that global is constructed — undefined behavior:
// file config.cpp
Config g_config; // may run AFTER the usage below
// file startup.cpp
extern Config g_config;
struct Startup {
Startup() { g_config.load(); } // undefined if g_config not yet constructed
} g_startup;The Meyers singleton avoids SIOF entirely: initialization is deferred to first call, which happens at a deterministic point in program execution — never before main(), and only when explicitly triggered.
Singleton Duplication Across Shared Libraries
On ELF platforms (Linux), if a Singleton lives in a static library (libfoo.a) that is linked into two separate shared libraries (libA.so and libB.so), each DSO gets its own copy of the object file and its own static local. The "one instance" guarantee is silently violated.
The fix: put the singleton in its own shared library and link both libA.so and libB.so against it dynamically. All consumers share the same code page and the same static variable.
The "Dead Reference" Problem
When one singleton's destructor calls another singleton that has already been destroyed, you read a destructed object — undefined behavior. This arises because the C++ standard does not define destruction order across translation units for static-duration objects.
Mitigations:
- Make singletons self-sufficient; avoid inter-singleton references during teardown.
- Use the
unique_ptrpattern with explicitreset()calls in a controlled shutdown sequence. - Structure dependencies so shorter-lived singletons are constructed (and therefore destroyed first) after longer-lived ones.
Double-Checked Locking — Do Not Write This
Pre-C++11, double-checked locking over a raw pointer was broken because the C++03 memory model gave no guarantees around out-of-order writes to the pointer versus the object's internal state. Post-C++11, the static-local guarantee makes it unnecessary. There is no modern C++ scenario where the following is the right choice:
// DO NOT use — both broken before C++11 and unnecessary after it
static Singleton* inst_ = nullptr;
static std::mutex mtx_;
static Singleton* instance() {
if (!inst_) { // potential data race on inst_
std::lock_guard lk(mtx_); // C++11
if (!inst_)
inst_ = new Singleton;
}
return inst_;
}
// The compiler generates equivalent or better code for: static Singleton inst;Recursive Initialization
If the singleton constructor (or anything it calls) triggers another call to instance() before the first initialization completes, the behavior is undefined — typically infinite recursion or a deadlock depending on the implementation. Keep the constructor simple and do not call instance() from within it.
See Also
- [[raii]] — destructor-based resource release that makes singleton teardown predictable
- [[policy-based]] — policy templates can parameterize singleton lifetime and thread-safety strategies
- [[dependency-injection]] — the preferred structural alternative when testability matters