Skip to content

Last updated:2026-10-13

The C++ Singleton Pattern

0. Introduction

Design patterns are an inescapable interview topic. This section covers the design pattern most frequently asked in interviews — the singleton pattern — including its classifications, thread safety, and thread-safe singleton solutions both from a pre-C++11 perspective and from a C++11-based perspective. Read this article carefully and you won't need to worry about it in future interviews.

1. The Well-Known Singleton

In most books, or as commonly known, the singleton pattern looks like this:

class singleton {
private:
    singleton() {}
    static singleton *p;
public:
    static singleton *instance();
};

singleton *singleton::p = nullptr;

singleton* singleton::instance() {
    if (p == nullptr)
        p = new singleton();
    return p;
}

This is a very simple implementation: the constructor is declared private or protected to prevent external functions from instantiating the class; inside, a static class pointer holds the unique instance; the instantiation is realized by a public method, which returns the sole instance of the class.

Of course this code is only suitable for single-threaded use; with multiple threads it is unsafe. Consider two threads calling instance for the first time at the same moment and both observing p as nullptr: both threads would construct an instance for p at the same time, which violates the rule of the singleton.

2. Lazy and Eager Initialization

Singletons come in two implementation styles:

  • Lazy initialization
  • The instance is created the first time it is needed; the code above is the lazy implementation.
  • Eager initialization
  • The instance is created at the time the singleton class is defined.

Here is the eager implementation as well:

class singleton {
private:
    singleton() {}
    static singleton *p;
public:
    static singleton *instance();
};

singleton *singleton::p = new singleton();
singleton* singleton::instance() {
    return p;
}

This one is of course thread-safe. The thread-unsafe variant we usually speak of is the lazy style; below we will go through thread-safe code optimizations for the lazy pattern.

3. Locking for Multiple Threads

In C++ there is a class for locking whose implementation principle is RAII, so you don't have to manage unlock manually — lock_guard; we use it here to lock.

class singleton {
private:
    singleton() {}
    static singleton *p;
    static mutex lock_;
public:
    static singleton *instance();
};

singleton *singleton::p = nullptr;

singleton* singleton::instance() {
    lock_guard<mutex> guard(lock_);
    if (p == nullptr)
        p = new singleton();
    return p;
}

With this version, the situation above — both threads executing into the p == nullptr branch — cannot happen. While thread A executes p = new Singleton(), if thread B calls instance(), it will definitely block at the lock and wait until thread A finishes and releases the lock. Hence it is thread-safe.

But this version performs very poorly, because every call to instance() acquires and releases the lock, while that step is only necessary for the first new Singleton(). Once p has been created, no matter how many threads access it concurrently, judging with if (p == nullptr) is enough (it is only a read operation, no lock needed); there is no thread-safety problem, and after adding the lock there is instead a performance problem.

This leads to DCL.

4. Double-Checked Locking Pattern

The version above always acquires the lock and then releases it no matter what, while for read operations there is no thread-safety concern, so locking is needed only when the instance is first created, and never afterwards. First, let's look at the DCLP implementation:

singleton* singleton::instance() {
    if(p == nullptr) {  // 第一次检查
        Lock lock;
        if(p == nullptr){ // 第二次检查
            p = new singleton;
        }
    }
    return p;
}

Building on the above, we can write double-checked locking + automatic reclamation

class singleton {
private:
    singleton() {}

    static singleton *p;
    static mutex lock_;
public:
    static singleton *instance();

    // 实现一个内嵌垃圾回收类
    class CGarbo
    {
    public:
        ~CGarbo()
        {
            if(singleton::p)
                delete singleton::p;
        }
    };
    static CGarbo Garbo; // 定义一个静态成员变量,程序结束时,系统会自动调用它的析构函数从而释放单例对象
};

singleton *singleton::p = nullptr;
singleton::CGarbo Garbo;
std::mutex singleton::lock_;

singleton* singleton::instance() {
    if (p == nullptr) {
        lock_guard<mutex> guard(lock_);
        if (p == nullptr)
            p = new singleton();
    }
    return p;
}

The key point of DCLP is that most calls to instance will see p as non-null, so they don't even try to initialize it. DCLP therefore checks whether p is null before attempting to acquire the lock. Only when that check succeeds (that is, p has not yet been initialized) does it acquire the lock, and then checks again whether p is still null (hence the name double-checked locking). The second check is necessary because, as we have just seen, another thread may well have initialized p after the first check and before the lock is successfully acquired.

The code above looks wonderful, yet quite some time passed before this flaw was discovered. The reason: out-of-order memory reads/writes (a compiler issue).

Consider again the line that initializes p:

p = new singleton;

This statement causes three things to happen:

  1. Allocate memory able to store the singleton object;
  2. Construct a singleton object in that allocated memory;
  3. Make p point to that allocated memory.

You might think these three steps execute in order, but in reality only step 1 is guaranteed to run first; steps 2 and 3 are not. That's where the problem lies.

  • Thread A calls instance, performs the first test of p, acquires the lock, executes steps 1 and 3, and is then suspended. At this moment p is non-null, but no Singleton object has been constructed yet in the memory p points to.
  • Thread B calls instance, sees p as non-null, and returns it to instance's caller. The caller dereferences the pointer to get the singleton — oops, an object that has not been constructed yet. There's the bug.

DCLP works well only when steps one and two are executed before step three, but there is no way to express that constraint in C or C++. It's like a dagger stuck in DCLP's heart: we need to define constraints on the relative order of instructions, but our language gives no way to express such constraints.

5. The memory barrier Instruction

The DCLP problem was solved in C++11.

Because the new C++11 specifies a new memory model, it is guaranteed that no thread switch happens while the three steps above execute — the initialization is effectively an "atomic" operation — and DCL can be used correctly again. However, under C++11 there is an even more concise way to write a multithreaded singleton, which we will introduce later.

Before C++11 the solution was the barrier instruction. To make it execute correctly, a memory barrier has to be inserted between steps 2 and 3, forcing the CPU to run in the order 1, 2, 3.

First implementation:

Based on operator new + placement new, written following the 1, 2, 3 execution order.

// method 1 operator new + placement new
singleton *instance() {
    if (p == nullptr) {
        lock_guard<mutex> guard(lock_);
        if (p == nullptr) {
            singleton *tmp = static_cast<singleton *>(operator new(sizeof(singleton)));
            new(tmp)singleton();
            p = tmp;
        }
    }
    return p;
}

Second implementation:

Based on directly embedding the ASM instruction mfence; Unix's barrier macro is also implemented through this instruction.

#define barrier() __asm__ volatile ("lwsync")
singleton *singleton::instance() {
    if (p == nullptr) {
        lock_guard<mutex> guard(lock_);
        barrier();
        if (p == nullptr) {
            p = new singleton();
        }
    }
    return p;
}

Usually this is an instruction provided by the CPU, and its role is to prevent the CPU from swapping instructions that come before it to after it; such an instruction is commonly called a barrier. In the code above, asm marks this as an assembly instruction; volatile is optional — if used, it declares to the compiler that this assembly instruction must not be optimized. lwsync is the barrier instruction provided by POWERPC.

6. Static Local Variables

Scott Meyers proposed a concise singleton writing style in Effective C++

singleton &singleton::instance() {
    static singleton p;
    return p;
}
  • In single-threaded use, correct.
  • Under multithreading in C++11 and later versions (e.g. C++14), correct.
  • Under multithreading before C++11, not necessarily correct.

The reason is that the standards before C++11 did not specify a memory model for local static variables, so it was not thread-safe. In C++11, however, it is thread-safe, because the new C++ standard stipulates that while one thread is initializing a variable, other threads must wait until that initialization completes before they can access it.

The memory orders used above:

  • memory_order_relaxed: relaxed memory order; only guarantees that operations on the atomic object are atomic
  • memory_order_acquire: an acquire operation; when reading an atomic object, no later reads or writes of the current thread may be reordered before this operation, and all memory writes by other threads before releasing the same atomic object become visible to the current thread
  • memory_order_release: a release operation; when writing an atomic object, no earlier reads or writes of the current thread may be reordered after this operation, and all memory writes of the current thread become visible to other threads that acquire the same atomic object

7. Atomic

In versions before C++11, besides implementing a thread-safe Singleton with locks, you can also make use of the atomic operations built into various compilers.

After discovering the reordering problem, java and c# added a keyword volatile: when declaring the variable p, it must be qualified with volatile, and once the compiler sees that, it knows this place must not be reordered (memory must be allocated first, then the constructor executed, and the assignment made only after everything is done).

The c++ standard, however, was never fixed, so VC++ also added this keyword in its 2005 release, but this is not portable (it only supports Microsoft platforms).

Then, with c++ 11, a memory model suitable for multithreading was introduced to eliminate these holes at the root. Finally there is such a mechanism to help us implement a portable solution.

mutex singleton::lock_;
atomic<singleton *> singleton::p;

/*
* std::atomic_thread_fence(std::memory_order_acquire); 
* std::atomic_thread_fence(std::memory_order_release);
* 这两句话可以保证他们之间的语句不会发生乱序执行。
*/
singleton *singleton::instance() {
    singleton *tmp = p.load(memory_order_relaxed);
    atomic_thread_fence(memory_order_acquire);
    if (tmp == nullptr) {
        lock_guard<mutex> guard(lock_);
        tmp = p.load(memory_order_relaxed);
        if (tmp == nullptr) {
            tmp = new singleton();
            atomic_thread_fence(memory_order_release);
            p.store(tmp, memory_order_relaxed);
        }
    }
    return p;
}

Notably, the code above uses two fairly key terms, acquire and release:

  • Acquire is a read operation on memory; no later reads or writes of the current thread may be reordered before it.
  • Release is a write operation on memory; no earlier reads or writes of the current thread may be reordered after it.

acquire and release usually appear in pairs, with the goal of guaranteeing that if a release on the same atomic object happens before an acquire, the memory modifications that happened before the release can all be seen by the memory reads after the acquire.

8. pthread_once

On Unix platforms, besides using atomic operations, if C++11 is not an option you can also implement the Singleton through pthread_once.

The prototype is as follows:

int pthread_once(pthread_once_t once_control, void (init_routine) (void));

Implementation:

class singleton {
private:
    singleton(); //私有构造函数,不允许使用者自己生成对象
    singleton(const singleton &other);

    //要写成静态方法的原因:类成员函数隐含传递this指针(第一个参数)
    static void init() {
        p = new singleton();
    }

    static pthread_once_t ponce_;
    static singleton *p; //静态成员变量 
public:
    singleton *instance() {
        // init函数只会执行一次
        pthread_once(&ponce_, &singleton::init);
        return p;
    }
};

9. Summary

This article covered several singleton patterns, explained thread-safe singleton patterns, and covered several thread-safe singleton implementations that do not use C++11: memory barrier, static local variables, the pthread_once approach, the C++11 atomic implementation, and so on.

Finally, note that for the destructor of the singleton classes above, refer to the double-checked locking + automatic reclamation implementation: you must declare a static local variable inside the class. A static local variable can be understood as a global variable; when the program ends, the destructor of that static local variable is called automatically. This is why a nested class is declared and defined inside the class instead of directly writing the singleton's destructor.

10. References

https://thorns.cn/2019/03/09/DCLP.html

https://www.cnblogs.com/liyuan989/p/4264889.html

https://segmentfault.com/a/1190000015950693

A Reusable Thread-Safe Singleton Template Class

Answering the request in issue #303: following the idea of boost::container::singleton_default, a CRTP template base class can turn any class into a thread-safe singleton in a single line (see singleton_default.hpp for the complete code, which includes concurrency tests):

template <class T>
class SingletonDefault {
public:
    static T& instance() {
        static T inst;   // C++11 起局部静态变量初始化线程安全
        return inst;
    }
    SingletonDefault(const SingletonDefault&) = delete;
    SingletonDefault& operator=(const SingletonDefault&) = delete;
protected:
    SingletonDefault() = default;
};

// 用法:继承即可
class MyConfig : public SingletonDefault<MyConfig> {
    friend class SingletonDefault<MyConfig>;
private:
    MyConfig() = default;   // 构造私有,交由基类创建
public:
    int value = 42;
};

// MyConfig::instance().value

Comments