C++
The Singleton Pattern in C++: One Instance, Global Access, Zero Chaos
Introduction
Some resources in a running program are inherently singular. A hardware device can only be opened once. A thread pool should have exactly one pool, not one per caller. A logger writing to a file must be the only one holding that file handle — two writers produce interleaved output that is nearly impossible to parse. The operating system or the hardware enforces these constraints physically, but the code has to enforce them logically. Left to convention alone, a codebase will eventually create a second instance of something that must be unique, and the resulting bugs tend to be subtle, non-deterministic, and discovered in production.
The Singleton pattern solves this by making the type itself responsible for the constraint. The class controls its own instantiation: the constructor is private, and the only way to reach the instance is through a single static access point. No external discipline required. This tutorial builds a game server logger from scratch, implements the pattern in three progressively better forms, and finishes with honest guidance on where the pattern earns its place and where it becomes a liability.
Background
A Singleton class enforces two properties simultaneously:
- Single instance: only one object of this type can exist in the process at any time
- Global access: that instance can be reached from anywhere in the code through a static method, without passing a reference
Both properties are implemented through the same mechanism: the constructor is declared private, which prevents any code outside the class from calling new Logger() or declaring Logger logger;. The class then exposes one static method — typically named get() or getInstance() — that creates the instance on first call and returns a reference to it on every subsequent call.
The subtlety is in that creation step. A naively written get() is not thread-safe, leaks memory, and cannot be configured before first use. Each of those limitations has a clean solution, and each is worth understanding before writing singleton code in a real codebase.
Practical Scenario
A game server runs three concurrent subsystems: physics, networking, and AI. Each subsystem emits log events — tick completions, collision detections, packet arrivals, pathfinding decisions. All of those events must flow into a single log stream with a sequential message counter, so that when a bug is reproduced from logs, the exact interleaving of events across subsystems is visible.
If each subsystem creates its own Logger, the message counter resets per instance, the output from different loggers interleaves unpredictably, and there is no single place to set the log verbosity level. The code compiles and runs, but the logs are unreliable as a diagnostic tool.
The Problem
The server currently lets each subsystem create its own logger. The Logger tracks which instance it is and counts its own messages independently.
Create a new file:
touch server.cpp
Compile and run with:
g++ -std=c++17 -o server server.cpp && ./server
#include <iostream>
#include <string>
class Logger {
public:
Logger() {
id_ = ++s_count_;
std::cout << " [Logger #" << id_ << " created]\n";
}
void log(const std::string& msg) {
std::cout << "[Logger #" << id_ << " | msg " << ++msg_count_ << "] " << msg << "\n";
}
private:
int id_ = 0;
int msg_count_ = 0;
static int s_count_;
};
int Logger::s_count_ = 0;
void run_physics() {
Logger logger;
logger.log("Physics tick processed");
logger.log("Collision detected");
}
void run_network() {
Logger logger;
logger.log("Packet received from client");
}
int main() {
Logger logger;
logger.log("Server starting");
run_physics();
run_network();
logger.log("Server shutting down");
return 0;
}
[Logger #1 created]
[Logger #1 | msg 1] Server starting
[Logger #2 created]
[Logger #2 | msg 1] Physics tick processed
[Logger #2 | msg 2] Collision detected
[Logger #3 created]
[Logger #3 | msg 1] Packet received from client
[Logger #1 | msg 2] Server shutting down
Three loggers are created. Each has its own message counter that starts at one. When replaying this log to trace a bug, "msg 1" appears three times with no relationship between them — there is no way to establish the actual order in which events occurred across subsystems. Setting the minimum log level to suppress debug output would require calling a setter on three different objects, and any subsystem that creates its logger after the setting is applied would revert to default behaviour. The problem compounds as the codebase grows.
Private Constructor and Static Access
The first step is to take instantiation away from callers entirely. Replace the Logger class with the following, and update the three functions and main to call Logger::get() instead of declaring local variables:
class Logger {
public:
static Logger& get() {
if (!s_instance_) {
s_instance_ = new Logger();
}
return *s_instance_;
}
void log(const std::string& msg) {
std::cout << "[msg " << ++msg_count_ << "] " << msg << "\n";
}
private:
Logger() { std::cout << " [Logger created]\n"; }
int msg_count_ = 0;
static Logger* s_instance_;
};
Logger* Logger::s_instance_ = nullptr;
void run_physics() {
Logger::get().log("Physics tick processed");
Logger::get().log("Collision detected");
}
void run_network() {
Logger::get().log("Packet received from client");
}
int main() {
Logger::get().log("Server starting");
run_physics();
run_network();
Logger::get().log("Server shutting down");
return 0;
}
[Logger created]
[msg 1] Server starting
[msg 2] Physics tick processed
[msg 3] Collision detected
[msg 4] Packet received from client
[msg 5] Server shutting down
One creation, five sequential messages. Any subsystem anywhere in the codebase that calls Logger::get() reaches the same object. The private constructor makes writing Logger logger; anywhere outside the class a compile error.
The type enforces its own uniqueness. No convention, no code review note, no comment saying "do not create more than one of these." The constraint is structural. A developer adding a new subsystem six months from now gets the same guarantee automatically.
Note: This implementation leaks the instance — s_instance_ is allocated with new and never freed. On most platforms this is harmless because the OS reclaims process memory on exit, but destructors that flush file buffers or release hardware handles will never run. The next section fixes this properly.
The Meyers Singleton
Scott Meyers documented a cleaner form in the 1990s that remains the canonical modern approach. Replace the get() method body and remove the static pointer declaration at file scope — they are no longer needed:
static Logger& get() {
static Logger instance;
return instance;
}
Remove the line Logger* Logger::s_instance_ = nullptr; from the file. Everything else stays the same.
[Logger created]
[msg 1] Server starting
[msg 2] Physics tick processed
[msg 3] Collision detected
[msg 4] Packet received from client
[msg 5] Server shutting down
The output is identical. The implementation is now three lines.
Why this is better
A function-local static in C++ has two properties that make it ideal here. First, it is constructed the first time execution reaches that line — lazy initialization for free. Second, since C++11 the standard guarantees that if two threads call get() simultaneously for the very first time, exactly one constructor runs and the other thread waits. No mutex, no std::call_once, no manual locking. The compiler emits the synchronisation. The instance is also destroyed at program exit in the reverse order of construction, so destructors that flush buffers or close handles run correctly.
Preventing Accidental Copies
A private constructor blocks direct construction, but it does not block copying. Without explicit deletion, this compiles silently:
Logger& ref = Logger::get(); // fine — a reference, no copy
Logger copy = Logger::get(); // copies the singleton into a new local object
The copy variable is a second, independent Logger with its own msg_count_ starting at zero. It is not the singleton. Add the following four declarations inside the public section of the class:
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
Logger(Logger&&) = delete;
Logger& operator=(Logger&&) = delete;
Now Logger copy = Logger::get(); produces a compile error:
error: use of deleted function 'Logger::Logger(const Logger&)'
Deleting these four operations closes the only remaining way to create a second instance. It also communicates intent to anyone reading the class: this type is non-copyable and non-movable by design, not by accident. Code that accidentally tries to store the singleton by value fails loudly at compile time rather than silently at runtime with a subtly wrong counter.
Controlled Initialization
The Meyers singleton constructs its instance the first time get() is called, with no way to pass configuration. For a logger, this means the minimum log level and output destination are hardcoded. Add log level filtering and a separate init() function that must be called before the logger is first used.
Replace the entire server.cpp with the following:
#include <iostream>
#include <string>
enum class Level { DEBUG, INFO, WARNING, ERROR };
class Logger {
public:
static void init(Level min_level) {
get().min_level_ = min_level;
}
static Logger& get() {
static Logger instance;
return instance;
}
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
Logger(Logger&&) = delete;
Logger& operator=(Logger&&) = delete;
void log(Level level, const std::string& msg) {
if (level < min_level_) return;
static const char* labels[] = {"DEBUG", "INFO", "WARN", "ERROR"};
std::cout << "[" << labels[static_cast<int>(level)] << " #"
<< ++msg_count_ << "] " << msg << "\n";
}
private:
Logger() { std::cout << " [Logger ready]\n"; }
Level min_level_ = Level::DEBUG;
int msg_count_ = 0;
};
void run_physics() {
Logger::get().log(Level::DEBUG, "Physics tick: 16ms");
Logger::get().log(Level::WARNING, "Physics collision threshold exceeded");
}
void run_network() {
Logger::get().log(Level::INFO, "Packet received from client");
Logger::get().log(Level::ERROR, "Network connection lost");
}
int main() {
Logger::init(Level::WARNING); // suppress DEBUG and INFO globally
Logger::get().log(Level::INFO, "Server starting");
run_physics();
run_network();
Logger::get().log(Level::INFO, "Server shutting down");
return 0;
}
[Logger ready]
[WARN #1] Physics collision threshold exceeded
[ERROR #2] Network connection lost
init() reaches the singleton through get() — there is no separate storage, no risk of configuring a different instance than the one subsystems will use. The INFO messages from run_network() and main are filtered because Level::INFO is below the configured Level::WARNING threshold. Changing the threshold in one place silently affects the entire program.
init() separates configuration from access. get() remains the sole path to the instance, and init() is simply the first thing main calls on that path. The alternative — passing configuration as arguments to get() and silently ignoring them after the first call — is harder to reason about and easier to misuse.
Note: Calling get() before init() is valid — the logger exists and uses Level::DEBUG as the default. Whether that is the right default depends on the application. For a server that ships to production, consider making the default Level::ERROR and requiring an explicit init() for debug builds.
A Second Pattern: The Connection Pool
The same structure applies to any resource that must be unique. A database connection pool manages a fixed number of connections. Two pools would each believe they own their full allocation, oversubscribing the database and causing connection failures under load. Replace the full contents of server.cpp:
#include <iostream>
#include <string>
class ConnectionPool {
public:
static void init(int capacity) {
get().available_ = capacity;
std::cout << " [Pool ready: " << capacity << " connections]\n";
}
static ConnectionPool& get() {
static ConnectionPool instance;
return instance;
}
ConnectionPool(const ConnectionPool&) = delete;
ConnectionPool& operator=(const ConnectionPool&) = delete;
ConnectionPool(ConnectionPool&&) = delete;
ConnectionPool& operator=(ConnectionPool&&) = delete;
std::string acquire() {
if (available_ == 0) return "(pool exhausted)";
--available_;
std::cout << " [acquired, " << available_ << " remaining]\n";
return "conn_" + std::to_string(++issued_);
}
void release() {
++available_;
std::cout << " [released, " << available_ << " available]\n";
}
private:
ConnectionPool() = default;
int available_ = 0;
int issued_ = 0;
};
void handle_request(const std::string& name) {
std::string conn = ConnectionPool::get().acquire();
std::cout << name << " using " << conn << "\n";
ConnectionPool::get().release();
}
int main() {
ConnectionPool::init(2);
handle_request("RequestA");
handle_request("RequestB");
std::string c1 = ConnectionPool::get().acquire();
std::string c2 = ConnectionPool::get().acquire();
std::string c3 = ConnectionPool::get().acquire(); // pool exhausted
std::cout << "c1=" << c1 << " c2=" << c2 << " c3=" << c3 << "\n";
return 0;
}
[Pool ready: 2 connections]
[acquired, 1 remaining]
RequestA using conn_1
[released, 2 available]
[acquired, 1 remaining]
RequestB using conn_2
[released, 2 available]
[acquired, 1 remaining]
[acquired, 0 remaining]
[acquired, 0 remaining]
c1=conn_3 c2=conn_4 c3=(pool exhausted)
The pool is the single source of truth for how many connections exist. If two ConnectionPool instances were created — one per request handler, for example — each would report two connections available while the database only permits two total. The exhaustion message would never appear, connections would be overissued, and the database would start refusing new connections with no clear signal in the application layer.
The ConnectionPool demonstrates the scenario where the singleton constraint is not a design preference but a physical requirement: the database server enforces a connection limit that only one object in the process can correctly track. The pattern makes this constraint explicit at the type level.
When to Reach for a Singleton — and When to Step Back
The pattern is appropriate when two conditions are both true: the resource is genuinely singular by nature, and global access simplifies the code without hiding important dependencies.
Good fits:
- Hardware interfaces — a serial port, a GPU context, an audio device. The operating system or driver enforces uniqueness; the singleton mirrors that constraint in code.
- Diagnostics and logging — a central log stream shared by all subsystems, where having one object is a correctness requirement, not just a convenience.
- Resource pools — a thread pool or connection pool where the capacity is a process-wide budget. Two pools would each claim the full budget.
- Global registries — a plugin registry or command dispatcher that must accumulate registrations from every module before any query is made.
Poor fits:
- Configuration — configuration is often passed as a struct or injected through constructors. A singleton config object makes unit-testing individual components impossible without mocking global state.
- Services you might want more than one of — a cache, a queue, a metrics collector. Systems that start with "there can only be one" frequently evolve into "we need one per region" or "one per tenant."
- Anything that needs to be replaced in tests — singletons with live state (open files, network sockets, hardware handles) break tests that run in isolation. If the type needs to be mocked or reset between tests, inject it instead.
The practical test: if the class could be instantiated twice and both instances would behave correctly without interfering with each other, it is not a singleton. Pass it by reference. Use a singleton when two instances of the class would be wrong — not inconvenient, not wasteful, but structurally incorrect.
Summary
The Singleton pattern enforces a uniqueness constraint inside the type itself rather than relying on callers to behave correctly. In this tutorial we built a game server logger and a connection pool that demonstrate the full implementation:
- A private constructor prevents any code outside the class from calling
new Logger()or declaringLogger logger;— the compiler enforces the constraint rather than convention - The Meyers singleton — a function-local
staticinsideget()— provides lazy initialization, correct destruction order, and C++11-guaranteed thread-safe construction in three lines with no manual locking - Deleting the copy constructor, copy assignment, move constructor, and move assignment closes the only remaining paths to creating a second instance; a
= deleteon all four is the complete and correct form - A separate
init()function called at startup separates configuration from access, reaching the instance throughget()so there is never a risk of configuring a different object than subsystems will use - The pattern is appropriate when two instances would be structurally wrong — a hardware device, a resource pool, a diagnostic stream — not merely when a single instance is convenient
- Singletons with live external state (file handles, sockets, hardware) make unit testing difficult; classes that could reasonably have two independent instances should be injected by reference instead