Skip to content
C++
Idiom
since C++11
Intermediate

Singleton Pattern

Thread-safe Singleton in C++ — Meyers singleton, SIOF, static local variable, phoenix singleton, monostate, registry, shared-library pitfalls, and when dependency injection wins.

Singletonsince C++11

A 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

cpp
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:

cpp
#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():

cpp
#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:

cpp
#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.

Non-Copyable, Non-Movable Enforcement

The private constructor alone is not sufficient to prevent duplication. Without deleted special members, a derived class or a friend could copy or move the instance. The complete set of deletions required is:

cpp
class Singleton {
public:
    static Singleton& instance() {
        static Singleton inst;   // C++11
        return inst;
    }

    // Prevent all forms of duplication                 // C++11
    Singleton(const Singleton&)            = delete;
    Singleton& operator=(const Singleton&) = delete;
    Singleton(Singleton&&)                 = delete;
    Singleton& operator=(Singleton&&)      = delete;

private:
    Singleton() = default;
};

Each deletion addresses a distinct risk:

  • Copy constructor — Singleton copy = Singleton::instance(); would compile without it.
  • Copy assignment — other = Singleton::instance(); where other is another Singleton&.
  • Move constructor — Singleton moved = std::move(Singleton::instance()); would create a second instance by moving the internals out.
  • Move assignment — other = std::move(Singleton::instance()); strips the state from the singleton entirely.

Deleting move operations does not prevent instance() from returning a reference. The reference itself is never moved; only explicit std::move expressions on that reference would trigger the move members, and the deleted declarations turn those into compile errors.

Static Initialization Order Fiasco (SIOF) and Why Meyers Singleton Solves It

Non-local static objects (globals) in different translation units initialize in an unspecified order. When one global's constructor depends on another global, you may observe the dependent object before the dependency is ready — undefined behavior with no diagnostic.

cpp
// file: database.cpp
Database g_database;   // initialized at some point before main()

// file: cache.cpp
extern Database g_database;

struct Cache {
    Cache() {
        g_database.connect("localhost");  // UB if g_database not yet constructed
    }
} g_cache;             // may run BEFORE g_database is constructed

The C++ standard gives no ordering guarantee between g_database (in database.cpp) and g_cache (in cache.cpp). The linker link order influences the actual order, but relying on it is fragile and non-portable.

The Meyers singleton eliminates SIOF entirely because function-local statics are initialized on first call, not at program startup. By the time any code can call instance(), the executable is running and all prior initializations are complete. The dependency is satisfied lazily:

cpp
// database.cpp
class Database {
public:
    static Database& instance() {
        static Database inst;   // C++11: initialized on first call, never before
        return inst;
    }
    void connect(const std::string& host) { /* ... */ }
private:
    Database() = default;
};

// cache.cpp
class Cache {
public:
    static Cache& instance() {
        static Cache inst;      // C++11
        return inst;
    }
private:
    Cache() {
        // Safe: Database::instance() is initialized here on first call,
        // guaranteed before Cache::instance() returns for the first time.
        Database::instance().connect("localhost");
    }
};

When Cache::instance() is first called, it enters the static-local initialization block for inst. During Cache's constructor, Database::instance() is called for the first time — which initializes the database singleton then and there. The order is now deterministic: Database is always constructed before Cache, regardless of link order or translation-unit layout.

Best Practice. Prefer Meyers singletons over global objects whenever a "global" is needed. The SIOF protection is automatic and costs nothing at runtime.

Destruction Order and the Phoenix Singleton

Static-duration objects are destroyed in the reverse order of their construction. If singleton A is constructed before singleton B, B is destroyed before A. This is well-defined for objects within the same translation unit, but across TUs the construction order is only as deterministic as the Meyers singleton pattern makes it (see above). If A's destructor calls B but B was constructed second (and therefore destroyed first), you read a destroyed object.

cpp
// A is constructed first (called first), so it is destroyed LAST.
// B is constructed second, so it is destroyed FIRST.
// If A's destructor uses B, that access is UB.
void shutdown() {
    A::instance();  // A constructed here
    B::instance();  // B constructed here
}                   // B destroyed, then A — A's dtor may access destroyed B

The phoenix singleton addresses this by resurrecting the object if it is accessed after destruction. A flag written at destruction time signals the need to reinitialize:

cpp
#include <cstdlib>   // std::atexit
#include <new>       // placement new

class PhoenixSingleton {
public:
    static PhoenixSingleton& instance() {
        if (!inst_) {
            inst_ = new (buf_) PhoenixSingleton{};   // placement new into aligned storage
            std::atexit(destroy);                     // schedule destruction
        }
        return *inst_;
    }

private:
    PhoenixSingleton() = default;

    static void destroy() noexcept {
        inst_->~PhoenixSingleton();   // explicit destructor call
        inst_ = nullptr;              // mark as dead — next access recreates it
    }

    // Aligned storage avoids dynamic allocation for resurrection
    alignas(PhoenixSingleton) static inline unsigned char buf_[sizeof(PhoenixSingleton)]; // C++17
    static inline PhoenixSingleton* inst_ = nullptr;                                       // C++17
};

When destroy() runs (registered via atexit), it sets inst_ to nullptr. If any subsequent destructor calls PhoenixSingleton::instance(), the check if (!inst_) triggers reconstruction into the same aligned storage. A new atexit call re-registers destruction. The phoenix rises as many times as needed.

Common Pitfall. Phoenix singletons trade one hazard (dead reference) for another (infinite destruction loops if two phoenixes each resurrect the other). Reserve this pattern for singletons that truly must outlive all consumers, such as loggers called from other destructors. The simpler mitigation — avoiding inter-singleton references during teardown — is always preferable when feasible.

Testability and Dependency Injection

The canonical anti-pattern is code that calls Config::instance() deep inside functions. This hard-codes the singleton as a hidden dependency. The function cannot be called in isolation without the singleton being initialized, and replacing the singleton with a test double requires modifying the singleton itself or using linker tricks.

cpp
// Untestable — hidden dependency on Config and Database singletons
void process_order(const Order& order) {
    auto max_qty = Config::instance().get<int>("max_order_qty");
    Database::instance().insert("orders", order.serialize());
    Logger::instance().log("order processed: " + order.id());
}

The refactoring is mechanical: extract the singleton calls to parameters with the singleton as the default at the call site:

cpp
// Testable — dependencies are explicit parameters
void process_order(const Order& order,
                   Config&   cfg = Config::instance(),
                   Database& db  = Database::instance(),
                   Logger&   log = Logger::instance()) {
    auto max_qty = cfg.get<int>("max_order_qty");
    db.insert("orders", order.serialize());
    log.log("order processed: " + order.id());
}

// Production — default arguments call the real singletons
process_order(order);

// Test — inject mocks without touching any singleton
MockConfig  mock_cfg;
MockDatabase mock_db;
MockLogger   mock_log;
process_order(order, mock_cfg, mock_db, mock_log);

For classes rather than free functions, inject via the constructor and store a reference:

cpp
class OrderProcessor {
public:
    explicit OrderProcessor(Config&   cfg = Config::instance(),
                            Database& db  = Database::instance(),
                            Logger&   log = Logger::instance())
        : cfg_{cfg}, db_{db}, log_{log} {}

    void process(const Order& order) {
        auto max_qty = cfg_.get<int>("max_order_qty");
        db_.insert("orders", order.serialize());
        log_.log("order processed: " + order.id());
    }

private:
    Config&   cfg_;
    Database& db_;
    Logger&   log_;
};

Best Practice. The singleton is the provider of the default instance; it does not have to be the only way to obtain that instance. Writing functions that accept references rather than calling instance() directly costs nothing in production (the compiler inlines the default argument) and makes every function independently testable.

Monostate as an Alternative

The monostate pattern achieves shared global state without restricting instantiation to one object. Every instance of the class reads and writes the same static data members, so all instances are interchangeable — but you can construct as many as you want.

cpp
class LoggerMonostate {
public:
    // No instance() — construct freely
    LoggerMonostate() = default;

    void log(std::string_view msg) const {
        std::lock_guard lk(mutex_);  // C++17: CTAD
        sink_ << "[" << level_name(level_) << "] " << msg << '\n';
    }

    void set_level(LogLevel lv) { level_ = lv; }
    LogLevel level() const      { return level_; }

private:
    // All instances share these statics
    inline static std::ostream*   sink_  = &std::clog;   // C++17
    inline static LogLevel        level_ = LogLevel::Info;
    inline static std::mutex      mutex_;

    static std::string_view level_name(LogLevel lv) { /* ... */ }
};

// Any instance reaches the same sink and level:
LoggerMonostate a, b;
a.set_level(LogLevel::Debug);
assert(b.level() == LogLevel::Debug);  // true — shared state

Contrast with the singleton:

PropertySingletonMonostate
Construction controlPrivate ctor, single instancePublic ctor, any number of instances
Access patternLogger::instance().log(...)LoggerMonostate{}.log(...) or stored instance
SubclassabilityDifficult (ctor is private)Normal inheritance
Reset/swap for testsRequires explicit reset()Replace static members directly
Intent clarityExplicitly "only one"Implicitly shared; harder to reason about

Best Practice. Prefer monostate when the number of instances should be unrestricted but the underlying resource is shared. The singleton's private constructor communicates "exactly one exists" at the type level — monostate does not carry this invariant, which can be a source of confusion.

Double-Checked Locking — History and Why It Was Broken

Before C++11, the Meyers singleton could not be used because the C++03 standard had no threading model. Developers invented double-checked locking (DCL) to lazily initialize a singleton with minimal lock overhead:

cpp
// DO NOT USE — broken before C++11, unnecessary after
static Singleton* inst_ = nullptr;
static std::mutex mtx_;          // C++11 mutex used here for illustration

static Singleton* instance() {
    if (inst_ == nullptr) {          // first check: no lock
        std::lock_guard lk(mtx_);   // acquire lock only if potentially uninitialized
        if (inst_ == nullptr) {      // second check: inside lock
            inst_ = new Singleton;
        }
    }
    return inst_;
}

The pattern appears correct but fails under C++03 because the compiler and CPU are free to reorder operations. The assignment inst_ = new Singleton is not atomic. It compiles into roughly three steps:

  1. Allocate raw memory.
  2. Construct the Singleton object in that memory.
  3. Write the pointer to inst_.

Steps 2 and 3 can be reordered. A second thread reading inst_ after step 3 but before step 2 sees a non-null pointer to unconstructed memory — and proceeds to use an uninitialized object.

C++11 introduced a formal memory model (<atomic>) that allows DCL to be written correctly using std::atomic<Singleton*> with memory_order_acquire/memory_order_release. But there is no reason to write any of this:

cpp
// C++11: the static-local guarantee replaces all of DCL
static Singleton& instance() {
    static Singleton inst;   // C++11: sequentially consistent, exactly once, thread-safe
    return inst;
}

The compiler generates the same kind of atomic test-and-set internally for function-local statics. The static-local form is shorter, has no raw pointers, cannot leak, and cannot be misused. DCL is exclusively historical at this point.

Registry Singleton

A registry singleton manages a collection of objects rather than a single resource. The canonical use case is a plugin architecture where plugins self-register at startup.

cpp
#include <functional>
#include <memory>
#include <stdexcept>
#include <string>
#include <unordered_map>

// Base class for all plugin types
class Plugin {
public:
    virtual ~Plugin() = default;
    virtual void execute() = 0;
};

// Factory registry: maps string names to factory functions
class PluginRegistry {
public:
    using Factory = std::function<std::unique_ptr<Plugin>()>;  // C++11

    static PluginRegistry& instance() {
        static PluginRegistry reg;   // C++11
        return reg;
    }

    void register_plugin(std::string name, Factory factory) {
        auto [it, ok] = factories_.emplace(std::move(name), std::move(factory));  // C++17
        if (!ok)
            throw std::logic_error{"duplicate plugin: " + it->first};
    }

    std::unique_ptr<Plugin> create(const std::string& name) const {
        auto it = factories_.find(name);
        if (it == factories_.end())
            throw std::out_of_range{"unknown plugin: " + name};
        return it->second();
    }

    PluginRegistry(const PluginRegistry&)            = delete;  // C++11
    PluginRegistry& operator=(const PluginRegistry&) = delete;
    PluginRegistry(PluginRegistry&&)                 = delete;
    PluginRegistry& operator=(PluginRegistry&&)      = delete;

private:
    PluginRegistry() = default;
    std::unordered_map<std::string, Factory> factories_;
};

// Helper: CRTP base that auto-registers on program startup
template<typename Derived>
struct AutoRegister {
    AutoRegister() {
        PluginRegistry::instance().register_plugin(
            Derived::plugin_name(),
            [] { return std::make_unique<Derived>(); }  // C++14
        );
    }
    inline static AutoRegister<Derived> registrar_{};   // C++17: inline static triggers ctor
};

// A concrete plugin — registration happens before main() via AutoRegister
class CompressorPlugin : public Plugin, private AutoRegister<CompressorPlugin> {
public:
    static std::string plugin_name() { return "compressor"; }
    void execute() override { /* compress */ }
};

With inline static AutoRegister<Derived> registrar_{} (C++17), the AutoRegister constructor runs when the translation unit that defines CompressorPlugin is loaded — before main() for static libraries, or on dlopen for shared libraries. The registry singleton is always ready because it uses function-local static initialization.

Common Pitfall. In a static library, a translation unit that defines only a self-registering plugin (and is not directly referenced by other code) may be dead-stripped by the linker before registrar_ is ever instantiated. Force linkage with --whole-archive (GNU ld), /WHOLEARCHIVE (MSVC), or -force_load (macOS ld).

Shared Library Pitfalls

A singleton defined in a header-only library can be silently duplicated when linked into multiple shared objects.

On Linux (ELF), if a static library (libfoo.a) containing the singleton's object file is linked into both libA.so and libB.so, each shared object gets its own copy of the object file. The function-local static in each copy is independent. An executable that loads both DSOs has two distinct singletons — one owned by each shared library.

cpp
libfoo.a  ──┬──> libA.so  (contains singleton.o with static inst_A)
            └──> libB.so  (contains singleton.o with static inst_B)

executable links libA.so + libB.so
→ two copies of the "singleton" exist

The fix is to place the singleton in its own shared library and link consumers dynamically against it:

cpp
libfoo.so  (singleton lives here — one copy, one static)
    ↑               ↑
libA.so         libB.so

On Linux, mark the symbol with default visibility so the dynamic linker can deduplicate it across DSOs:

cpp
// singleton.h — exported from libfoo.so
class __attribute__((visibility("default"))) Config {  // GCC/Clang, ELF
public:
    static Config& instance();
    // ...
};

// singleton.cpp — compiled into libfoo.so, not libA.so or libB.so
Config& Config::instance() {
    static Config inst;
    return inst;
}

On Windows (PE/COFF), use __declspec(dllexport) in the defining DLL and __declspec(dllimport) in consumers:

cpp
// Portable export macro
#ifdef _WIN32
  #ifdef LIBFOO_BUILDING
    #define LIBFOO_API __declspec(dllexport)
  #else
    #define LIBFOO_API __declspec(dllimport)
  #endif
#else
  #define LIBFOO_API __attribute__((visibility("default")))
#endif

class LIBFOO_API Config {
public:
    static Config& instance();
    // ...
};

On macOS, the default symbol visibility for shared libraries is already public. However, using -fvisibility=hidden (a common compile flag for symbol hygiene) requires explicit __attribute__((visibility("default"))) annotations just as on Linux.

Best Practice. If the singleton must be shared across DSO boundaries, define its instance() function body in a .cpp file compiled into exactly one shared library. Never put a function-local static definition in a header file that might be included by code compiled into multiple DSOs.

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:

cpp
// 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 shared library pitfall above.

Common Pitfalls

Static Initialization Order Fiasco (SIOF) with Globals

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:

cpp
// 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_ptr pattern with explicit reset() calls in a controlled shutdown sequence.
  • Structure dependencies so shorter-lived singletons are constructed (and therefore destroyed first) after longer-lived ones.
  • Use the phoenix singleton pattern for singletons that are called from other destructors and must outlive all consumers.

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:

cpp
// 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