To implement the Singleton design pattern in C++, you ensure a class has only one instance and provide a global access point to it. Below are two common approaches, including thread-safe solutions for modern C++ (C++11 and later).
1. Meyer’s Singleton (Modern C++ and Thread-Safe)
This approach leverages static local variables initialized in a thread-safe manner (guaranteed by C++11 and later).
Code Example:
class Singleton {
private:
// Private constructor to prevent direct instantiation
Singleton() = default;
// Delete copy constructor and assignment operator
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
public:
// Access the single instance
static Singleton& getInstance() {
static Singleton instance; // Thread-safe initialization (C++11+)
return instance;
}
// Example method
void doSomething() {
// Implementation here
}
};
// Usage:
Singleton::getInstance().doSomething();
Key Features:
- Thread-Safe: Static local variable initialization is atomic in C++11+.
- Automatic Destruction: Instance is destroyed when the program exits.
- No Manual Cleanup: No need for
delete
ordestroyInstance()
.
2. Classic Singleton with Manual Control
Use this approach if you need explicit lifetime management (not recommended for most cases).
Code Example (Thread-Safe with Double-Checked Locking):
#include <mutex>
#include <atomic> // For atomic operations
class Singleton {
private:
static std::atomic<Singleton*> instance;
static std::mutex mtx;
// Private constructor
Singleton() = default;
// Delete copy/assignment
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
public:
// Get the instance (thread-safe)
static Singleton* getInstance() {
Singleton* ptr = instance.load(std::memory_order_acquire);
if (!ptr) {
std::lock_guard<std::mutex> lock(mtx);
ptr = instance.load(std::memory_order_relaxed);
if (!ptr) {
ptr = new Singleton();
instance.store(ptr, std::memory_order_release);
}
}
return ptr;
}
// Explicitly destroy the instance
static void destroyInstance() {
std::lock_guard<std::mutex> lock(mtx);
delete instance.load();
instance = nullptr;
}
// Example method
void doSomething() {
// Implementation here
}
};
// Initialize static members
std::atomic<Singleton*> Singleton::instance(nullptr);
std::mutex Singleton::mtx;
// Usage:
Singleton* s = Singleton::getInstance();
s->doSomething();
Singleton::destroyInstance();
Key Features:
- Double-Checked Locking: Minimizes locking overhead after initialization.
- Manual Destruction: Requires explicit cleanup with
destroyInstance()
. - Atomic Operations: Ensures thread safety during instance creation.
Key Considerations
- Thread Safety:
- Use Meyer’s Singleton for simplicity and automatic thread safety.
- Use Double-Checked Locking for manual control (rarely needed).
- Destruction:
- Meyer’s Singleton: Destructor runs at program exit.
- Manual Singleton: Call
destroyInstance()
explicitly.
- Anti-Patterns:
- Avoid global mutable state where possible (Singletons can introduce tight coupling).
- Prefer dependency injection for testability.
When to Use Singletons
- Logging Systems: Centralized logging across an application.
- Database Connections: Single shared connection pool.
- Configuration Managers: Global access to settings.
Full Example: Thread-Safe Logger (Meyer’s Approach)
#include <iostream>
#include <fstream>
#include <string>
class Logger {
private:
std::ofstream logFile;
Logger() {
logFile.open("app.log", std::ios::app);
if (!logFile.is_open()) {
throw std::runtime_error("Failed to open log file");
}
}
~Logger() {
if (logFile.is_open()) {
logFile.close();
}
}
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
public:
static Logger& getInstance() {
static Logger instance;
return instance;
}
void log(const std::string& message) {
logFile << message << std::endl;
}
};
// Usage:
Logger::getInstance().log("Application started");
Summary
- Preferred Method: Meyer’s Singleton (simple, thread-safe, automatic cleanup).
- Manual Control: Classic Singleton with double-checked locking (rarely needed).
- Avoid Pitfalls: Ensure thread safety and proper destruction.