Singleton Pattern
- Description: One instance per process, global access point — widely considered an anti-pattern; covers Meyers singleton, thread safety, testability problems, and modern alternatives (DI,
std::call_once,inlinevariable). - My Notion Note ID: K2C-2-1
- Created: 2026-05-22
- Updated: 2026-05-22
- License: Reuse is very welcome. Please credit Yu Zhang and link back to the original on yuzhang.io
Table of Contents
- 1. The Problem
- 2. Structure
- 3. C++ Implementations
- 4. Thread Safety
- 5. When to Use, When Not To
- 6. Why Singleton Is an Anti-Pattern
- 7. Related Patterns
- 8. References
1. The Problem
- Some resources need exactly one instance per process — logger, config registry, hardware controller, thread pool, GPU context.
- Plain global object → eager init, undefined cross-TU init order ("static initialization order fiasco"), no lazy construction.
- Want: lazy init + single instance + controlled access through one well-known entry point.
2. Structure
| Role | Responsibility |
|---|---|
Singleton class |
Holds the single instance; private ctor; deletes copy/move. |
instance() |
Static accessor — returns reference to the one instance. |
| Storage | Function-local static, class-static pointer, or inline variable. |
- One class plays all roles. No collaborators in the GoF diagram.
3. C++ Implementations
3.1 Meyers singleton (preferred)
- Function-local
static→ thread-safe init guaranteed since C++11 ([N2660] "magic statics"). - Destruction order = reverse of first-use order across TUs.
class Logger {
public:
static Logger& instance() {
static Logger inst; // initialized on first call, thread-safe
return inst;
}
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
Logger(Logger&&) = delete;
Logger& operator=(Logger&&) = delete;
void log(std::string_view msg) { /* ... */ }
private:
Logger() = default;
~Logger() = default;
};
// Usage
Logger::instance().log("hello");
- Delete copy + move explicitly. Without that,
auto x = Logger::instance();silently copies (if copy ctor exists) and you have two.
3.2 GoF classic form (avoid)
class Logger {
public:
static Logger* instance() {
if (!inst_) inst_ = new Logger; // race in multi-threaded code
return inst_;
}
private:
Logger() = default;
static Logger* inst_;
};
Logger* Logger::inst_ = nullptr;
- Race on the null check + assignment. Naive double-checked locking is broken on weakly-ordered memory models without
std::atomic. Thenewis never freed → leaks until process exit.
3.3 inline static (C++17)
class Config {
public:
static Config& instance() { return inst_; }
private:
Config() = default;
inline static Config inst_{}; // eager, but single definition across TUs
};
- Eager — runs before
mainenters. Subject to static initialization order fiasco ifinst_depends on other globals across TUs.
4. Thread Safety
- Meyers singleton (C++11+) — fully thread-safe init. Compiler emits an internal guard variable + atomic flag.
- Methods on the singleton — not auto-thread-safe. Add
std::mutexper method or design the interface as immutable. - Double-checked locking (DCLP) — historically broken in C++03; correct in C++11 only with
std::atomic+ acquire/release ordering. Even when correct, more code than Meyers for no gain.
// Correct DCLP — but Meyers singleton subsumes this. Shown for context.
static std::atomic<Logger*> ptr{nullptr};
static std::mutex m;
Logger* get() {
Logger* p = ptr.load(std::memory_order_acquire);
if (!p) {
std::lock_guard lk{m};
p = ptr.load(std::memory_order_relaxed);
if (!p) {
p = new Logger;
ptr.store(p, std::memory_order_release);
}
}
return p;
}
std::call_once+std::once_flagis the readable alternative if Meyers isn't viable (e.g. parameterized init).
static std::once_flag flag;
static std::unique_ptr<Logger> inst;
Logger& Logger::instance() {
std::call_once(flag, []{ inst = std::make_unique<Logger>(); });
return *inst;
}
5. When to Use, When Not To
Use when:
- External resource genuinely is single — physical device, hardware MMIO, OS-level mutex, single GPU context.
- Cross-cutting infrastructure — logger, metrics sink — and dependency injection is overkill for the project (small CLI tool, throwaway script).
Avoid when:
- "We only need one" is a current observation, not a hard requirement. Tomorrow's requirement is two.
- The class holds mutable state that tests want to reset or fake out.
- The class is reached from many call sites that don't otherwise know about it → hidden coupling.
6. Why Singleton Is an Anti-Pattern
- Hidden global state — function signatures lie.
void foo()actually reads fromLogger::instance()andConfig::instance(). Reasoning about side effects requires reading the body. - Testability — code under test reaches into
instance(); tests can't substitute a fake. Either expose areset_for_test()(ugly) or refactor to take the dependency as a parameter (defeats the singleton). - Lifetime tangles — static dtor order across TUs is unspecified. Singleton A using singleton B in its dtor → UB if B died first.
- Concurrency footguns — single instance amplifies contention on its mutex.
- Hostile to parallel testing —
pytest -p xdist/ GoogleTest sharding spawn multiple test workers sharing the singleton state.
Modern alternative: dependency injection. Pass the logger / config as a constructor parameter (or via a small composition root). Production wires one instance; tests wire a fake.
struct App {
explicit App(Logger& log, Config& cfg) : log_{log}, cfg_{cfg} {}
void run() { log_.log("starting"); /* ... */ }
private:
Logger& log_;
Config& cfg_;
};
int main() {
Logger log;
Config cfg;
App{log, cfg}.run(); // one place to wire, no Singleton class needed
}
- The "single instance" property is enforced by
mainconstructing one, not by the class.
7. Related Patterns
- Singleton vs static class (only static members, no instances) — static class has no construction phase → no lazy init, no polymorphism, can't satisfy an interface. Singleton can hide behind an abstract base; static class can't.
- Singleton vs Monostate — Monostate = every instance shares the same static data; clients construct freely. Less surprising than Singleton because the "one instance" detail is invisible.
- Singleton vs Service Locator — Service Locator is a registry of singletons keyed by type. Same hidden-global-state critique applies, but more flexible (can swap implementations at startup).
- Singleton vs Dependency Injection — DI inverts ownership: caller decides who provides the dependency. Eliminates the hidden global; trades it for plumbing.
- Singleton vs Factory Method — Factory returns a fresh instance per call (or a configured one). Singleton always returns the same instance.
8. References
- Singleton — refactoring.guru
- Singleton — sourcemaking
- N2660 — Dynamic Initialization and Destruction with Concurrency (C++11)
- Meyers, Effective C++, Item 4 — "Make sure that objects are initialized before they're used"
- cppreference —
std::call_once - Alexandrescu, "Double-Checked Locking, Threads, Compiler Optimizations, and More"
- GoF, Design Patterns: Elements of Reusable Object-Oriented Software, ch. 3 — Singleton