Singleton Pattern

The Singleton pattern ensures a class has exactly one instance and provides a global access point to that instance. In modern C++ (C++11+), the most reliable and idiomatic implementation is the Meyers Singleton using a function-local static.


Why Do We Need Singleton?

The design requirement

Some components represent shared system resources or single points of coordination. Allowing multiple instances can cause inconsistent state, duplicated resource usage, or conflicting behavior.

Typical valid use cases

  • Logging (single sink, consistent formatting/config)
  • Configuration registry (single source of truth)
  • Telemetry/tracing dispatcher
  • Hardware device manager (exclusive access to a device)
  • Global service locator (use with caution)

What problems does Singleton prevent?

  • State divergence: two "global" instances hold different configurations
  • Resource conflicts: multiple objects writing to the same file/device
  • Coordination failure: multiple schedulers/dispatchers competing

Engineering note: Singleton is a tool. Overuse creates a hidden global state, tight coupling, and test friction.


How to Use Singleton in C++

The recommended usage style

Call a static accessor and work with a reference:

Logger::getInstance().log("System started");

The recommended implementation (Meyers Singleton, C++11+)

#include <iostream>
#include <string>

class Logger {
public:
    static Logger& getInstance() {
        static Logger instance; // thread-safe initialization since C++11
        return instance;
    }

    void log(const std::string& msg) {
        std::cout << msg << '\n';
    }

    Logger(const Logger&) = delete;
    Logger& operator=(const Logger&) = delete;

private:
    Logger() = default;
};
  • Private constructor prevents direct creation.
  • Deleted copy/assign prevents duplication.
  • Function-local static provides lazy initialization and safe lifetime management.

3. How Does Singleton Work?

Core mechanism

The accessor contains a static local instance. The first call constructs it; subsequent calls return the same object. In C++11 and later, the language guarantees thread-safe initialization of function-local statics.

Lifetime and destruction

  • The instance is typically destroyed automatically at program shutdown.
  • Be cautious about the destruction order of static objects across translation units (the “static initialization order fiasco”).
  • If other static objects depend on your Singleton during shutdown, consider designs that avoid access during shutdown.

4. UML (Graphical)

The following UML diagram is provided as an inline SVG image (no external files required).

Singleton - Singleton() - (copy ctor) = delete + getInstance(): Singleton& + (other operations…) Notes: • Private ctor blocks external creation • Static accessor returns the single instance • Copy/assign disabled to prevent duplicates
Figure: UML structure of a classic Singleton.

5. Singleton Template (Reusable C++ Code)

A reusable Singleton base template can reduce boilerplate. This pattern is common but should be used carefully (it introduces inheritance and “friend” usage).

template <typename T>
class Singleton {
public:
    static T& getInstance() {
        static T instance;
        return instance;
    }

    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

protected:
    Singleton() = default;
    ~Singleton() = default;
};

Usage:

class ConfigManager : public Singleton<ConfigManager> {
    friend class Singleton<ConfigManager>;
private:
    ConfigManager() = default;
public:
    int getPort() const { return 8080; }
};

6. Examples: Correct Use vs Incorrect Use

Correct use example: Logger

Logging is commonly global: a single sink, consistent formatting, and shared configuration.

Logger::getInstance().log("Start up");

Incorrect use example: Database connection

Making a database connection a Singleton is often a mistake because modern systems benefit from:

  • Connection pooling (multiple connections for concurrency)
  • Per-request context (transactions, timeouts, retries)
  • Testability (mocking/stubbing DB interactions)
// Often a poor design choice:
Database::getInstance().query("SELECT * FROM users");

Better approaches include dependency injection (DI), repositories, and explicit resource management (pool objects).

Another common misuse: "Everything is a Singleton"

Turning business services into singletons creates hidden coupling and global mutable state. This typically makes unit tests brittle and increases the risk of cross-test contamination.


7. Double-Checked Locking (DCL)

Why DCL existed

Before C++11, function-local static initialization was not guaranteed to be thread-safe, and the language memory model was less formally specified. Developers used DCL to reduce locking overhead while still lazily creating the instance.

Classic DCL example (historical)

#include <mutex>

class SingletonDCL {
private:
    static SingletonDCL* instance;
    static std::mutex mtx;

    SingletonDCL() = default;

public:
    static SingletonDCL* getInstance() {
        if (instance == nullptr) {                  // 1st check (no lock)
            std::lock_guard<std::mutex> lock(mtx);
            if (instance == nullptr) {              // 2nd check (with lock)
                instance = new SingletonDCL();
            }
        }
        return instance;
    }
};

// Definitions (typically in a .cpp)
SingletonDCL* SingletonDCL::instance = nullptr;
std::mutex SingletonDCL::mtx;

Why classic DCL is dangerous

  • Historically vulnerable to instruction reordering and visibility issues.
  • One thread could observe a non-null pointer to an object that is not fully constructed.
  • Requires correct memory ordering and often std::atomic to be safe.

In modern C++ (C++11+), prefer function-local static unless you have a specialized requirement.


8. Singleton vs Double-Checked Locking

AspectMeyers Singleton (C++11+)Double-Checked Locking (DCL)
Thread-safe initialization Yes (language-guaranteed) Only if implemented carefully (often needs atomics)
Complexity Low High
Performance overhead Typically minimal; no per-call lock Intended to reduce locking, but adds complexity
Correctness risk Low Historically error-prone
Recommended today Yes (default choice) Rarely, only for special cases

9. Thread-safe vs Non-thread-safe Singleton (Comparison)

Non-thread-safe lazy initialization (DO NOT use in multithreaded code)

The following is a classic broken approach. Two threads could both see instance == nullptr and create two objects.

class BrokenSingleton {
private:
    static BrokenSingleton* instance;
    BrokenSingleton() = default;

public:
    static BrokenSingleton* getInstance() {
        if (!instance) {                // race condition
            instance = new BrokenSingleton();
        }
        return instance;
    }
};

Thread-safe singleton (recommended)

class SafeSingleton {
public:
    static SafeSingleton& getInstance() {
        static SafeSingleton instance;  // thread-safe since C++11
        return instance;
    }
    SafeSingleton(const SafeSingleton&) = delete;
    SafeSingleton& operator=(const SafeSingleton&) = delete;

private:
    SafeSingleton() = default;
};

Practical guidance

  • If your program is multithreaded (most are), assume singleton access can be concurrent.
  • Prefer language-guaranteed thread-safe initialization over manual locking.
  • Avoid heap allocation and manual lifetime unless required.

10. Singleton in Multithreaded High-Performance Systems

In high-performance systems (trading, games, low-latency services, embedded control loops), Singleton usage is not “good” or “bad” by itself—what matters is contention, false sharing, allocation strategy, and access patterns.

Key concerns

  • Hot path contention: if many threads frequently call singleton methods that lock a mutex, performance collapses.
  • Global state bottleneck: a singleton can become an implicit serialization point.
  • Cache locality: frequently accessed shared state can cause cache-line bouncing between cores.
  • Shutdown order: In complex applications, singleton destructors can run after dependencies are gone.

Engineering patterns that work well

  • Initialization once, then lock-free reads: build immutable configuration data at startup; expose const/atomic reads.
  • Partitioned state: use per-thread buffers (TLS) for logs/metrics and merge asynchronously.
  • Explicit dependencies: pass references to services into components (DI) for testability and clarity, even if the service is globally owned.

Recommendation

Use Meyers Singleton for lifetime control and single-instance guarantee, but design the singleton’s internal operations to avoid locks on hot paths. If you need synchronization, prefer designs where the singleton only coordinates, while the heavy work is handled by thread-local or lock-free components.


11. Summary

  • Default choice (C++11+): Meyers Singleton with function-local static.
  • Avoid: non-thread-safe lazy initialization and classic DCL unless you truly need it.
  • Use cautiously: Singleton introduces global access; overuse harms testability and design clarity.
  • For high-performance multithreading: avoid contention and global bottlenecks; prefer immutable data and partitioned state.

© 2026 Air Supply Information Center (Air Supply BBS)