How do you implement the Singleton design pattern in C++?

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 or destroyInstance().

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

  1. Thread Safety:
  • Use Meyer’s Singleton for simplicity and automatic thread safety.
  • Use Double-Checked Locking for manual control (rarely needed).
  1. Destruction:
  • Meyer’s Singleton: Destructor runs at program exit.
  • Manual Singleton: Call destroyInstance() explicitly.
  1. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *