A collection of minimal, self-contained C++ examples demonstrating multiple ways to implement the Singleton design pattern. The repository includes modern, thread-safe techniques (Meyer's Singleton) and legacy approaches (Double-Checked Locking) for comparison.
| Example | Initialization | Storage duration | Cleanup | Thread-safe Initialization | Notes |
|---|---|---|---|---|---|
singleton-classic-example |
Eager | Static | Automatic | Yes | Can suffer from SIOF and static destruction order issues. |
singleton-meyers-example |
Lazy | Static | Automatic | Yes (since C++11) |
Avoids classic SIOF because the object is created on first use. May still suffer from static destruction order issues during program shutdown. |
singleton-classic-dynamic-example |
Lazy | Dynamic | Manual | No |
Races may occur if multiple threads call getInstance() or
delInstance() concurrently.Manual lifetime management is error-prone. |
singleton-cherno-example |
Lazy | Dynamic | Manual | No |
Similar problems to singleton-classic-dynamic-example: manual cleanup,
unsafe lifetime management and no thread safety.
|
singleton-dclp-example |
Lazy | Dynamic | Manual | No |
Classic Double-Checked Locking Pattern (DCLP). Historically unsafe because of data races and reordering issues. Obsolete in modern C++. |
singleton-leaky-example |
Lazy | Dynamic | None | Yes (since C++11) |
Intentionally leaks memory to avoid shutdown-order problems. Useful when destruction order is more dangerous than not deleting the instance. |
🔳 Singleton with a static member instance.
- 🧩 Variable: Static member variable
- 💾 Storage: Static storage duration
- ⚡ Initialization: Eager → Created before
main()starts - 🧼 Cleanup: Automatic → Destroyed after
main()exits - 🔒 Thread safety: Construction is effectively safe because initialization happens before
main()in a single-threaded context ⚠️ Static Initialization Order Fiasco (SIOF):- If a static object in another translation unit accesses the singleton during its own initialization, the singleton may not be constructed yet.
⚠️ Static Destruction Order Fiasco:- If a static object in another translation unit accesses the singleton during its own destruction, the singleton may already have been destroyed.
🔳 Meyer's Singleton → The simplest and safest modern C++ singleton implementation.
- 🧩 Variable: Static local variable
- 💾 Storage: Static storage duration
- ⏳ Initialization: Lazy → Created only on first call to
getInstance() - 🧼 Cleanup: Automatic → Destroyed after
main()exits - 🔒 Thread safety: Initialization is thread-safe since C++11
- A function-local static variable is initialized exactly once, even in a multi-threaded environment.
- ✅ Fixes SIOF: Avoids the classic static initialization order problem because the object is created on first use.
⚠️ Still vulnerable to Static Destruction Order Fiasco
🔳 Singleton with a static member pointer → Dynamically allocated on first use.
- 🧩 Variable: Static member pointer
- 💾 Storage: Dynamic storage duration
- ⏳ Initialization: Lazy → Created only on first call to
getInstance() - 🧹 Cleanup: Manual → Requires manual destruction via
delInstance() ⚠️ Thread safety: Not guaranteed- Two threads could call
delInstance()simultaneously, or one callsgetInstance()while another callsdelInstance(), leading to a race on the pointer.
- Two threads could call
⚠️ Manual lifetime management is error-prone- Because the singleton is destroyed explicitly via
delInstance(), the program must ensure no code still uses the old instance after deletion.
- Because the singleton is destroyed explicitly via
🔳 Cherno-style Singleton — inspired by Why I don't like Singletons - Cherno
- 🧩 Variable: Static global pointer
- 💾 Storage: Dynamic storage duration
- ⏳ Initialization: Lazy → Created only on first call to
getInstance() - 🧹 Cleanup: Manual → Requires manual destruction via
delInstance() ⚠️ Thread safety: Not guaranteed, same as singleton-classic-dynamic-example⚠️ Manual lifetime management is error-prone → Same as singleton-classic-dynamic-example
🔳 Singleton with Double-Checked Locking Pattern (DCLP) → Classic, but unsafe lazy-initialization pattern.
- 🧩 Variable: Static member pointer
- 💾 Storage: Dynamic storage duration
- ⏳ Initialization: Lazy → Created only on first call to
getInstance() - 🧹 Cleanup: Manual → Requires manual destruction via
delInstance() ⚠️ Thread safety: Not guaranteed, suffers from data races and reordering issues.- ❌ DCLP is unreliable → Multiple threads may observe a partially constructed object.
- ⛔ Largely obsolete since C++11 → Thread-safe function-local static initialization is the simpler and preferred modern solution.
- 📖 Reference: C++ and the Perils of Double-Checked Locking by Scott Meyers and Andrei Alexandrescu
🔳 "Leaky" Singleton → A heap-allocated singleton initialized through a function-local static pointer.
- 🧩 Variable: Static local pointer
- 💾 Storage: Dynamic storage duration
- ⏳ Initialization: Lazy → Created only on first call to
getInstance() - 🧹 Cleanup: No cleanup, the object is intentionally never deleted.
- The memory is intentionally left allocated until process termination.
- In practice, the operating system reclaims the memory when the process exits.
- 🔒 Thread safety: Initialization is thread-safe since C++11
- The local static pointer initialization is still protected by C++11's thread-safe static init guarantee, so the
newonly fires once, safely.
- The local static pointer initialization is still protected by C++11's thread-safe static init guarantee, so the
- ✅ Avoids both construction and destruction order problems
- Because the object is created on first use, it avoids SIOF
- Because it is never destroyed, it avoids static destruction order issues
⚠️ Trade-off: intentionally leaks memory by design
Before building, ensure you have the following installed:
- CMake (v3.20 or newer required for Presets)
- C++ compiler supporting C++20 (required for
syncstream)
- Visual Studio 2022
- Workload Required: Desktop development with C++
- Note: The preset uses the Visual Studio 17 2022 generator.
- MinGW-w64 Toolchain
- Generator: Ninja
- C++ Compiler (GCC/Clang)
- Generator: Ninja
The commands below are executed from the root of the repository.
- 🖥️ Windows (MSVC) →
cmake --preset windows-msvc - 🖥️ Windows (GCC, Ninja) →
cmake --preset windows-gcc-debug - 🖥️ Windows (Clang, Ninja) →
cmake --preset windows-clang-debug - 🐧 Linux (GCC) →
cmake --preset linux-gcc-debug - 🐧 Linux (Clang) →
cmake --preset linux-clang-debug
- 🖥️ Windows (MSVC) →
cmake --build --preset windows-msvc-debug - 🖥️ Windows (GCC, Ninja) →
cmake --build --preset windows-gcc-debug - 🖥️ Windows (Clang, Ninja) →
cmake --build --preset windows-clang-debug - 🐧 Linux (GCC) →
cmake --build --preset linux-gcc-debug - 🐧 Linux (Clang) →
cmake --build --preset linux-clang-debug
cmake --build --preset <preset> --target <target_name>Example:
cmake --build --preset linux-gcc-debug --target 02-singleton-meyers-examplebuild/windows-msvc/02-singleton-meyers-example/Debug/02-singleton-meyers-example.exebuild/windows-gcc-debug/02-singleton-meyers-example/02-singleton-meyers-example.exebuild/windows-clang-debug/02-singleton-meyers-example/02-singleton-meyers-example.exe./build/linux-gcc-debug/02-singleton-meyers-example/02-singleton-meyers-example./build/linux-clang-debug/02-singleton-meyers-example/02-singleton-meyers-example