Implementing the Singleton Design Pattern in C++
The Singleton pattern is a creational design pattern intended to ensure that a class has exactly one instance while providing a global point of access to that instance. This pattern is particularly valuable when coordinating actions across a system or managing shared resources where multiple instances would lead to inconsistent state or excessive resource consumption.
Fundamental Concepts of Singletons
The pattern is defined by three primary characteristics:
- Controlled Instantiation: The class prevents external entities from creating new instances by making its constructer private. It manages its own lifecycle.
- Unique Instance Storage: A private static member within the class holds the reference to the single existing object.
- Static Access Method: A public static function (often named
getInstance) provides the mechanism for users to retrieve the instance. If the instance does not exist, the class creates it; otherwise, it returns the existing one.
Common Use Cases
- Configuration Managers: Centralizing application settings to ensure consistency.
- Hardware Interface Access: Managing access to a specific piece of hardware (like a serial port or printer) where concurrent access must be serialized.
- Connection Pooling: Managing a cache of database connections to improve performance and limit resource usage.
- Audit Logging: Directing all log messages through a single service to maintain chronological order and centralize output handling.
Implementation Variants in C++
Lazy Initialization (Non-Thread-Safe)
In a basic lazy implementation, the instance is created only when it is requested for the first time. This saves resources if the object is never used.
#include <iostream>
class BasicManager {
public:
static BasicManager* get_instance() {
if (m_instance == nullptr) {
m_instance = new BasicManager();
}
return m_instance;
}
private:
BasicManager() = default;
~BasicManager() = default;
// Prohibit copying and assignment
BasicManager(const BasicManager&) = delete;
BasicManager& operator=(const BasicManager&) = delete;
static BasicManager* m_instance;
};
BasicManager* BasicManager::m_instance = nullptr;
Note that this version is not safe for multi-threaded environments, as two threads might simultaneously check if m_instance is null and create two separate objects.
Eager Initialization
Eager initialization creates the instance as soon as the program starts (during static initialization). This approach is inherently thread-safe in most scenarios because the instance is created before the main function executes.
class EagerProvider {
public:
static EagerProvider& fetch() {
return m_handle;
}
private:
EagerProvider() = default;
EagerProvider(const EagerProvider&) = delete;
EagerProvider& operator=(const EagerProvider&) = delete;
static EagerProvider m_handle;
};
EagerProvider EagerProvider::m_handle;
Thread-Safe Lazy Initialization (Double-Checked Locking)
To ensure thread safety while maintaining lazy initialization, double-checked locking can be used alongside a mutex. This reduces overhead by only locking during the first creation phase.
#include <mutex>
#include <memory>
class SecureRegistry {
public:
static SecureRegistry& get_access() {
if (m_ptr == nullptr) {
std::lock_guard<std::mutex> lock(m_sync_obj);
if (m_ptr == nullptr) {
m_ptr.reset(new SecureRegistry());
}
}
return *m_ptr;
}
private:
SecureRegistry() = default;
SecureRegistry(const SecureRegistry&) = delete;
SecureRegistry& operator=(const SecureRegistry&) = delete;
static std::unique_ptr<SecureRegistry> m_ptr;
static std::mutex m_sync_obj;
};
std::unique_ptr<SecureRegistry> SecureRegistry::m_ptr = nullptr;
std::mutex SecureRegistry::m_sync_obj;
Modern C++ Thread-Safe Singleton (Meyers Singleton)
Since C++11, the initialization of static local variables is guaranteed to be thread-safe. This is the cleanest and most efficient way to implement a singleton in modern C++.
class ModernSingleton {
public:
static ModernSingleton& instance() {
static ModernSingleton obj;
return obj;
}
private:
ModernSingleton() = default;
ModernSingleton(const ModernSingleton&) = delete;
ModernSingleton& operator=(const ModernSingleton&) = delete;
};
Practical Example: Global Audit Logger
This example demonstrates a thread-safe logging utility that writes system events to a local file using std::call_once for initialization.
#include <iostream>
#include <fstream>
#include <string>
#include <mutex>
#include <memory>
class SystemAuditLogger {
public:
static SystemAuditLogger& get_logger() {
std::call_once(m_init_flag, []() {
m_instance.reset(new SystemAuditLogger());
});
return *m_instance;
}
void record_event(const std::string& log_entry) {
std::lock_guard<std::mutex> lock(m_file_mutex);
std::ofstream file("audit_log.txt", std::ios::app);
if (file.is_open()) {
file << "[LOG]: " << log_entry << std::endl;
}
}
private:
SystemAuditLogger() = default;
SystemAuditLogger(const SystemAuditLogger&) = delete;
SystemAuditLogger& operator=(const SystemAuditLogger&) = delete;
static std::unique_ptr<SystemAuditLogger> m_instance;
static std::once_flag m_init_flag;
std::mutex m_file_mutex;
};
std::unique_ptr<SystemAuditLogger> SystemAuditLogger::m_instance = nullptr;
std::once_flag SystemAuditLogger::m_init_flag;
// Usage function
void emit_log(const std::string& text) {
SystemAuditLogger::get_logger().record_event(text);
}
Evaluation of the Singleton Pattern
Advantages
- Resource Efficiency: Prevents unnecessary instantiation of heavy objects.
- Namespace Cleanliness: Avoids polluting the global namespace with global variables while offering similar accessibility.
- Controlled Access: Provides a strict mechanism for interacting with a sensitive resource.
Disadvantages
- Tight Coupling: Code that depends on a singleton becomes harder to test because the dependency is hidden and difficult to mock.
- State Persistence: Singletons maintain state for the entire duration of the application, wich can lead to side effects in unit tests.
- Violation of Single Responsibility: The class is responsible both for its core business logic and for managing its own lifecycle.