Singleton
Learning Objectives
Implement a thread-safe lazy singleton in C++ and compare other languages.
Explain lifetime, testing and global-state problems.
Replace singletons with dependency injection where appropriate.
Prerequisites: Modules 10, 22.
Intuition and Definition
Intent: ensure a class has exactly one instance and provide a global access point to it.
Forces: some resources are genuinely unique (a hardware register block, a process-wide configuration); creating several would be wrong or wasteful. But global access couples every caller to the concrete class and hides dependencies.
Participants: the Singleton class with a private constructor and a static instance() accessor.
Why It Matters
Singleton is the most used and most criticised pattern. Knowing its pitfalls — hidden dependencies, test pollution, initialization and destruction order, concurrency — is a standard interview topic.
Syntax and Language Notes
class Config {
public:
static Config& instance() { static Config c; return c; } // C++11: thread-safe lazy init
Config(const Config&) = delete;
Config& operator=(const Config&) = delete;
private:
Config() = default;
};// Java (not run): the enum singleton is serialization- and reflection-safe
public enum Registry { INSTANCE; public void register(String s) { } }public sealed class Registry
{
private static readonly Lazy<Registry> lazy = new(() => new Registry()); // thread-safe lazy
public static Registry Instance => lazy.Value;
private Registry() { }
}# a module is already a singleton: import config; config.value = ...Complete Example
#include <iostream>
#include <map>
#include <string>
#include <thread>
#include <vector>
class DiagnosticLog {
public:
static DiagnosticLog& instance() {
static DiagnosticLog log; // constructed once, on first call, thread-safe
return log;
}
DiagnosticLog(const DiagnosticLog&) = delete;
DiagnosticLog& operator=(const DiagnosticLog&) = delete;
void record(const std::string& code) { ++counts_[code]; } // NOT thread-safe by itself!
int count(const std::string& code) const { auto it = counts_.find(code); return it == counts_.end() ? 0 : it->second; }
private:
DiagnosticLog() { ++constructions; }
std::map<std::string, int> counts_;
public:
static inline int constructions = 0;
};
// the DI alternative: depend on an interface, let main decide it's shared
struct Log { virtual ~Log() = default; virtual void record(const std::string&) = 0; };
struct MemoryLog : Log { std::vector<std::string> codes; void record(const std::string& c) override { codes.push_back(c); } };
class Sensor {
Log& log_;
public:
explicit Sensor(Log& l) : log_(l) {}
void fault() { log_.record("P0117"); }
};
int main() {
std::vector<std::thr
…constructions after 8 threads: 1
U0100 count: 2
same object: 1
injected log entries: 2Step-by-Step Walkthrough
Eight threads call instance() simultaneously; the C++11 "magic static" guarantees one construction.
Thread-safe *creation* does not make *use* thread-safe: record modifies a std::map and would need a mutex if called concurrently.
The Sensor alternative receives a Log&. The application still shares one log, but tests can pass a fresh MemoryLog and nothing depends on a global.
classDiagram
class DiagnosticLog {
-DiagnosticLog()
+instance()$ DiagnosticLog
+record(code)
}
Client ..> DiagnosticLog : instance()Common Mistakes
Using Singleton for convenience (global access) rather than because one instance is *required*.
Hidden dependencies: functions that call X::instance() can't be tested without the real X.
Test pollution: state left in the singleton by one test leaks into the next.
Double-checked locking hand-rolled with non-atomic pointers (broken before C++11/Java 5 memory models).
Destruction-order problems: another static's destructor using the singleton after it was destroyed.
Trade-offs and Alternatives
Dependency injection of a shared instance Almost always for application code
Module-level object (Python, JS modules) Monostate (all-static data, normal objects) Rare; same global-state issues Container singleton lifetime (DI)
Real-World Use
Legitimate cases: a hardware abstraction for a unique peripheral, a process-wide logger *backend*, std::cout. Even then, prefer passing a reference to code that uses it.
Practice
Q1. (Easy) Why is the constructor private?
Answer: So no code outside the class can create additional instances.
Q2. (Medium) Is static T& instance() { static T t; return t; } safe if two threads call it at the same time?
Answer: Yes for initialization (C++11 guarantees it); member functions still need their own synchronization.
Q3. (Advanced) How would you make a singleton-dependent function testable without rewriting all callers at once?
Answer: Introduce an interface for the singleton's operations, give the function an overload or default parameter that accepts the interface (defaulting to the singleton), and migrate callers gradually — "parameterize from above". Tests pass a fake.
Interview Question
Q1. "Why is Singleton often called an anti-pattern?" Answer: Because its *global access point* is the problem, not the single instance: it hides dependencies, couples callers to a concrete class, makes unit tests share state and order-dependent, complicates concurrency (shared mutable state), and has lifetime issues at shutdown. The single-instance requirement is better met by creating one object at the composition root and injecting it.
Quick Quiz
Q1. What makes the C++ function-local static thread-safe?
Answer: The C++11 standard guarantees concurrent initialization of block-scope statics happens exactly once.
Q2. What is the Java enum singleton good for?
Answer: Serialization and reflection safety with no extra code.
Q3. Better alternative for most application code?
Answer: Dependency injection of one shared instance.
Summary
Singleton = one instance + global access; the global access is the costly part.
Use magic statics for safe lazy creation; still synchronize use.
Prefer DI; reserve singletons for truly unique resources.
Related: Module 10, Module 22, Module 60.