Implementing Thread-Safe Singleton Patterns in Modern C++

The Singleton pattern enforces the existence of only one instance of a class throughout an application's lifetime, providing a single, global access point.

Core Requirements

  • Ensure a single global instance by restricting object creation (private constructor) and disabling copy/move semantics (deleted copy constructor and assignment operator).
  • Provide a unique access point via a static member function.
  • Guarantee proper resource cleanup with one-time destruction.

Implementation Categories

Implementations generally fall in to two categories based on initialization timing.

Eager Initialization Singleton

This method initializes the singleton instance before the main function begins execution.

#include <iostream>

class EagerSingleton {
public:
    static EagerSingleton& getInstance() {
        return singleton_instance;
    }

    void performAction() const {
        std::cout << "EagerSingleton action performed." << std::endl;
    }

private:
    EagerSingleton() = default;
    ~EagerSingleton() = default;
    EagerSingleton(const EagerSingleton&) = delete;
    EagerSingleton& operator=(const EagerSingleton&) = delete;

    static EagerSingleton singleton_instance;
};

// Definition and initialization of the static member.
EagerSingleton EagerSingleton::singleton_instance;

int main() {
    EagerSingleton::getInstance().performAction();
    return 0;
}

Advantages:

  • Simple implementation with inherent thread safety due to initialization before main.

Disadvantages:

  • Potential memory overhead if the object is never used.
  • Subject to the "Static Initialization Order Fiasco" where dependencies between static objects in different translation units can lead to undefined behavior due to unspecified initialization order.
  • Not suitable for header-only libraries without C++17's inline variables, as the static member requires a separate definition.

Note on Destruction Dependencies: Even using "Construct On First Use" with local static variables can fail if a static object's destructor depends on another static object that may have already been destroyed. The safest approach is to minimize global static objects with interdependencies.

Lazy Initialization Singleton

This method creates the instance only upon the first request, addressing the memory overhead issue but introducing complexity around thread safety and controlled destruction.

Basic Non-Thread-Safe Version

class UnsafeLazySingleton {
public:
    static UnsafeLazySingleton* getInstance() {
        if (instance_ptr == nullptr) { // Not thread-safe.
            instance_ptr = new UnsafeLazySingleton();
        }
        return instance_ptr;
    }

private:
    UnsafeLazySingleton() = default;
    ~UnsafeLazySingleton() = default;
    static UnsafeLazySingleton* instance_ptr;
};

UnsafeLazySingleton* UnsafeLazySingleton::instance_ptr = nullptr;

Thread-Safe Lazy Singleton with Mutex

A straightforward but inefficient solution uses a mutex lock on every access.

#include <mutex>

class LockedLazySingleton {
public:
    static LockedLazySingleton* getInstance() {
        std::lock_guard<std::mutex> lock(instance_mutex);
        if (instance_ptr == nullptr) {
            instance_ptr = new LockedLazySingleton();
        }
        return instance_ptr;
    }

private:
    LockedLazySingleton() = default;
    ~LockedLazySingleton() = default;
    static LockedLazySingleton* instance_ptr;
    static std::mutex instance_mutex;
};

LockedLazySingleton* LockedLazySingleton::instance_ptr = nullptr;
std::mutex LockedLazySingleton::instance_mutex;

The Double-Checked Locking Pitfall (DCLP)

Attempting to optimize by checking the pointer before locking leads to a subtle race condition due to instruction reordering by the compiler or CPU.

class DCLPSingleton {
public:
    static DCLPSingleton* getInstance() {
        // First check (unsafe without memory barriers).
        if (instance_ptr == nullptr) {
            std::lock_guard<std::mutex> lock(instance_mutex);
            // Second check inside the lock.
            if (instance_ptr == nullptr) {
                // Problem: `new` is not atomic. A thread might see
                // `instance_ptr` assigned before the constructor finishes.
                instance_ptr = new DCLPSingleton();
            }
        }
        return instance_ptr;
    }

private:
    DCLPSingleton() = default;
    ~DCLPSingleton() = default;
    static DCLPSingleton* instance_ptr;
    static std::mutex instance_mutex;
};

A correct DCLP implementation requires memory barriers to enforce ordering, making it complex. Additionally, managing the destruction of the new-allocated object and ensuring correct order relative to other static destructors is challenging.

The Modern C++11 Solution

C++11 guarantees that the initialization of a block-scope static variable (local static) is thread-safe. This provides a simple, safe, and efficient lazy singleton.

class ModernSingleton {
public:
    static ModernSingleton& getInstance() {
        // Thread-safe initialization guaranteed by C++11.
        static ModernSingleton unique_instance;
        return unique_instance;
    }

    void operation() const {
        std::cout << "ModernSingleton operation." << std::endl;
    }

private:
    ModernSingleton() {
        std::cout << "ModernSingleton constructed." << std::endl;
    }
    ~ModernSingleton() {
        std::cout << "ModernSingleton destroyed." << std::endl;
    }
    ModernSingleton(const ModernSingleton&) = delete;
    ModernSingleton& operator=(const ModernSingleton&) = delete;
};

Benefits:

  • Thread-safe construction without explicit locks.
  • Destruction happens in the reverse order of construction, respecting dependencies.
  • The instance is destoryed automatically at program termination.

Preventing Explicit Deletion

To prevent a user from accidentally calling delete &ModernSingleton::getInstance(), make the destructor private. Alternatively, overload and privatize the operator delete.

class ProtectedSingleton {
public:
    static ProtectedSingleton& getInstance() {
        static ProtectedSingleton unique_instance;
        return unique_instance;
    }
    // ... public methods ...
private:
    ProtectedSingleton() = default;
    ~ProtectedSingleton() = default; // Private destructor prevents `delete`.
    // ... disable copy/move ...
};

Generic Singleton Template using CRTP

The Curious Recurring Template Pattern (CRTP) can create a reusable base class for singletons.

template <typename Derived>
class SingletonBase {
protected:
    SingletonBase() = default;
    virtual ~SingletonBase() = default;

public:
    static Derived& getInstance() {
        // The wrapper inherits from Derived to access protected constructor.
        struct Wrapper : public Derived {
            Wrapper() : Derived() {}
        };
        static Wrapper instance_storage;
        return static_cast<Derived&>(instance_storage);
    }

    SingletonBase(const SingletonBase&) = delete;
    SingletonBase& operator=(const SingletonBase&) = delete;
};

// Usage: The concrete class must inherit using CRTP and have protected constructor/destructor.
class ApplicationManager : public SingletonBase<ApplicationManager> {
    // Friend declaration allows SingletonBase<ApplicationManager> to call the constructor.
    friend class SingletonBase<ApplicationManager>;

protected:
    ApplicationManager() {
        std::cout << "ApplicationManager created." << std::endl;
    }
    ~ApplicationManager() override {
        std::cout << "ApplicationManager destroyed." << std::endl;
    }

public:
    void run() {
        std::cout << "ApplicationManager running." << std::endl;
    }
};

int main() {
    ApplicationManager::getInstance().run();
    return 0;
}

Tags: C++ Design Patterns singleton Thread Safety CRTP

Posted on Thu, 21 May 2026 21:59:42 +0000 by Awestruck