GoF Design Patterns - Singleton
A practical guide to the Singleton pattern from the GoF collection, with C++ examples, thread-safety notes, usages, anti-patterns, and mermaid diagrams.
Hello! I'm Pan-kun.
This article covers the Singleton pattern from the GoF (Gang of Four) design patterns series.
It provides practical guidance including C++ example code, how to implement it, when to use it, important caveats, and visualizations using mermaid diagrams.
Introduction
The Singleton pattern ensures that a class has only one instance within an application and provides a global access point to that single instance.
Key aspects that typically define a Singleton implementation:
- Private constructor: prevents direct instantiation from outside.
- Static instance: the class holds the single instance.
- Static accessor: a function to provide access to the single instance.
Wikipedia defines it as:
"In software engineering, the singleton pattern is a software design pattern that restricts the instantiation of a class to one 'single' instance."
(Source: https://en.wikipedia.org/wiki/Singleton_pattern)
From the GoF perspective, the pattern centralizes control of object creation and access, which is useful for shared resources such as configuration, loggers, or connection pools.
Article goals:
- Show representative C++ implementation patterns and their pros/cons
- Explain thread-safe initialization, performance, and testability concerns
- Outline alternatives (e.g., Dependency Injection) and anti-patterns
Primary references (selected):
- https://en.wikipedia.org/wiki/Singleton_pattern
- https://en.cppreference.com/w/cpp/language/storage_duration#Static_local_variables
- https://en.cppreference.com/w/cpp/thread/call_once
These references serve as the basis for the implementations and design guidance below.
Typical Use Cases
- Centralized configuration management
- Shared logger instance
- Hardware resources or connection pools where a single manager instance suffices
- Application-wide caches or factories
Because a Singleton creates a global instance, it can make testing harder. Before adopting it, always ask: "Is it really necessary?" and "Is this the single instance that should be globally accessible?"
C++ Implementation Patterns
Several implementation strategies are common for the Singleton pattern:
- Eager initialization
- Lazy initialization
- Double-checked locking (DCL)
- Initialization-on-demand holder (Meyers' Singleton style)
Below are a few C++ examples simplified for learning.
Eager Singleton (simple)
Eager singleton is straightforward. In this approach the instance is created at program startup (static initialization). That means the creation cost happens during startup, and you may face static initialization order issues in complex programs.
// EagerSingleton.h
#pragma once
#include <iostream>
class EagerSingleton {
private:
static EagerSingleton instance; // created at program startup
EagerSingleton() = default;
public:
EagerSingleton(const EagerSingleton&) = delete;
EagerSingleton& operator=(const EagerSingleton&) = delete;
static EagerSingleton& GetInstance() {
return instance;
}
void Say() { std::cout << "EagerSingleton\n"; }
};
// EagerSingleton.cpp
EagerSingleton EagerSingleton::instance; // definition requiredLazy Singleton with mutex
A lazy singleton delays instance creation until first use. The example below uses a mutex to ensure thread safety. This approach is correct but incurs lock overhead on every access, which might be a performance concern.
// LazySingleton.h
#pragma once
#include <mutex>
#include <memory>
class LazySingleton {
private:
static std::mutex mtx;
static LazySingleton* instance;
LazySingleton() = default;
public:
LazySingleton(const LazySingleton&) = delete;
LazySingleton& operator=(const LazySingleton&) = delete;
static LazySingleton* GetInstance() {
std::lock_guard<std::mutex> lock(mtx);
if (!instance) instance = new LazySingleton();
return instance;
}
void Say();
};
// LazySingleton.cpp
std::mutex LazySingleton::mtx;
LazySingleton* LazySingleton::instance = nullptr;This pattern may be appropriate if:
- You can accept the extra lock cost, or
- Your environment's performance characteristics make the lock cost negligible.
Meyers' Singleton (recommended for C++11+)
Meyers' Singleton uses a function-local static variable. Since C++11, initialization of function-local static variables is thread-safe, so this provides a concise, safe lazy-initialization approach.
// MeyersSingleton.h
#pragma once
#include <iostream>
class MeyersSingleton {
private:
MeyersSingleton() { }
public:
MeyersSingleton(const MeyersSingleton&) = delete;
MeyersSingleton& operator=(const MeyersSingleton&) = delete;
static MeyersSingleton& GetInstance() {
static MeyersSingleton instance; // thread-safe since C++11
return instance;
}
void Say() { std::cout << "MeyersSingleton\n"; }
};Refer to cppreference for the guarantee that local static initialization is thread-safe: https://en.cppreference.com/w/cpp/language/storage_duration#Static_local_variables
Lazy Singleton with std::call_once
std::call_once and std::once_flag provide an explicit and safe way to perform one-time initialization. This is useful for more complex initialization logic.
// CallOnceSingleton.h
#pragma once
#include <mutex>
#include <memory>
class CallOnceSingleton {
private:
static std::unique_ptr<CallOnceSingleton> instance;
static std::once_flag initFlag;
CallOnceSingleton() = default;
static void Init() {
instance.reset(new CallOnceSingleton());
}
public:
CallOnceSingleton(const CallOnceSingleton&) = delete;
CallOnceSingleton& operator=(const CallOnceSingleton&) = delete;
static CallOnceSingleton& GetInstance() {
std::call_once(initFlag, &CallOnceSingleton::Init);
return *instance;
}
void Say();
};
// CallOnceSingleton.cpp
std::unique_ptr<CallOnceSingleton> CallOnceSingleton::instance;
std::once_flag CallOnceSingleton::initFlag;Useful reference: https://en.cppreference.com/w/cpp/thread/call_once
Practical extensions and variations
Once you understand the basic pattern you can adapt it to various needs. The following are examples and not necessarily the best solution for every situation.
Template-based eager singleton: avoids repeating singleton boilerplate but may reduce readability as it hides the singleton nature in a template.
// TemplateEagerSingleton.h
#pragma once
#include <iostream>
template<typename T>
class TemplateEagerSingleton {
private:
static T instance; // created at program startup
TemplateEagerSingleton() = delete;
public:
TemplateEagerSingleton(const TemplateEagerSingleton&) = delete;
TemplateEagerSingleton& operator=(const TemplateEagerSingleton&) = delete;
static T& GetInstance() {
return instance;
}
};
// static member definition (for templates, definition can be in header)
template<typename T>
T TemplateEagerSingleton<T>::instance;
// Foo.h
struct Foo {
void Say() { std::cout << "TemplateEagerSingleton MyService\n"; }
};
// other.cpp
auto& foo = TemplateEagerSingleton<Foo>::GetInstance();
foo.Say();Multi-instance eager approach: allows a fixed number of globally accessible instances. This changes the semantics from strict singleton to a small pool of global instances — lifecycle management and use-case justification should be considered.
// MultiInstanceEagerSingleton.h
#pragma once
#include <array>
#include <iostream>
#include <cstddef>
#include <cassert>
class MultiInstanceEagerSingleton {
private:
static constexpr std::size_t COUNT = 3;
static std::array<MultiInstanceEagerSingleton, COUNT> instances; // created at startup
int id_;
MultiInstanceEagerSingleton(int id = 0) : id_(id) {}
public:
MultiInstanceEagerSingleton(const MultiInstanceEagerSingleton&) = delete;
MultiInstanceEagerSingleton& operator=(const MultiInstanceEagerSingleton&) = delete;
// Access one of several instances by index
static MultiInstanceEagerSingleton& GetInstance(std::size_t index) {
assert(index < COUNT && "index out of range");
return instances[index];
}
void Say() { std::cout << "MultiInstanceEagerSingleton id=" << id_ << '\n'; }
};
// MultiInstanceEagerSingleton.cpp
#include "MultiInstanceEagerSingleton.h"
// static member definition (COUNT instances created at startup)
std::array<MultiInstanceEagerSingleton, MultiInstanceEagerSingleton::COUNT> MultiInstanceEagerSingleton::instances = {
MultiInstanceEagerSingleton(0),
MultiInstanceEagerSingleton(1),
MultiInstanceEagerSingleton(2)
};
// other.cpp
auto& s0 = MultiInstanceEagerSingleton::GetInstance(0);
s0.Say();When to use and alternatives
Before adopting a Singleton, consider:
- Does the application truly need exactly one instance?
- Do tests need to substitute the instance?
- Is thread-safe initialization guaranteed for your chosen approach?
Alternatives:
- Dependency Injection (DI): Pass dependencies into classes so tests can inject mocks/stubs.
- Factory / Service Locator: Provides centralized management; Service Locator can hide global dependencies so use with caution.
- Module-scoped singletons: In C++, managing a shared object at module/translation-unit scope may suffice.
Testability
Singletons introduce global state, which can make unit testing harder. Consider:
- Providing test-only initialization/teardown hooks (careful to avoid leaking test-only APIs into production).
- Allowing construction to be supplied externally so tests can inject mocks.
- Meyers' Singleton (function-local static) lives until process exit and is difficult to reset; consider process-level isolation for tests or using DI for testability.
Anti-patterns and pitfalls
- Overuse: Wrapping global variables in a Singleton just for convenience increases coupling.
- Hidden global state: Dependencies become implicit, making testing and refactoring hard.
- Lifecycle ambiguity: Static initialization order issues and unclear shutdown semantics when multiple static objects interact.
Summary
- Singleton enforces a single instance and provides a global access point, but it comes with design trade-offs: testability, coupling, and lifecycle concerns.
- In C++11 and later, the function-local static (Meyers' Singleton) provides a concise, thread-safe option.
- Use
std::call_oncefor explicit and robust one-time initialization when you need more control. - For large systems or when testability is a priority, prefer DI or factory-based approaches over singletons.
References
- "Singleton pattern" — Wikipedia: https://en.wikipedia.org/wiki/Singleton_pattern
- Quote: "In software engineering, the singleton pattern is a software design pattern that restricts the instantiation of a class to one 'single' instance."
- C++: Static local variables (thread-safety): https://en.cppreference.com/w/cpp/language/storage_duration#Static_local_variables
- Quote: "Since C++11, the initialization of local static variables is thread-safe."
- C++: std::call_once: https://en.cppreference.com/w/cpp/thread/call_once
- Quote: "Ensures that the function f is called exactly once, even if called from several threads."
Mermaid diagrams
Class diagram (Singleton and consumer relationship):
%%{init:{'theme':'default'}}%%
classDiagram
class MeyersSingleton {
+static MeyersSingleton& GetInstance()
+void Say()
}
class Consumer {
+void useSingleton()
}
Consumer --> MeyersSingleton : usesSequence diagram (first call vs subsequent calls):
sequenceDiagram
participant Client
participant Singleton
Client->>Singleton: GetInstance() // 1st call
alt not initialized
Singleton-->>Singleton: initialize (constructor)
Singleton-->>Client: return instance
else already initialized
Singleton-->>Client: return instance
endThat’s it for this article. Next time we'll cover another GoF pattern with C++ examples and mermaid diagrams. The implementations shown here are simplified for learning; evaluate requirements (threads, lifecycle, testability) carefully before adopting in production.
— Pan-kun
Loading comments...