C++单例模板

单例的写法基本都相同,定义一个模板方便使用单例模式。


分析

单例模式需要确保资源初始化是线程安全的,因而出现了以下几种方法(选自《C++ 单例模式的模板实现》、《单例模式很简单?但你真能写对吗?》):

1. 直接加锁

1
2
3
4
5
6
7
8
//线程安全版本,但锁的代价过高
Singleton* Singleton::GetInstance() {
    Lock lock; //伪代码 加锁
    if (instance == nullptr) {
        instance = new Singleton();
    }
    return instance;
}

虽然确保了初始化只有一次,但是每次GetInstance的时候都会上锁,导致性能不高。

2. 双检查锁

既然初始化后不需要再加锁,那么上锁之前增加一个判断if (instance == nullptr),资源未初始化才上锁并初始化,并且在初始化之前再判断一次资源是否为空,确保如果多个线程同时进入了第一个if时,只有一个线程会初始化资源。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
//双检查锁,但由于内存读写reorder不安全
Singleton* Singleton::GetInstance() {
    //先判断是不是初始化了,如果初始化过,就再也不会使用锁了
    if(instance==nullptr){
        Lock lock; //伪代码
        if (instance == nullptr) {
            instance = new Singleton();
        }
    }
    return instance;
}

看起来很完美,不是吗。但是经过编译器优化后的程序,并不一定按照你想的那样工作。instance = new Singleton();可以分成三个步骤执行:

  1. 分配了一个Singleton类型对象所需要的内存。
  2. 在分配的内存处构造Singleton类型的对象。
  3. 把分配的内存的地址赋给指针instance。

由于编译器的优化,可能出现内存读写的乱序执行,只能确保步骤1最先执行。假如线程A按1,3,2的顺序执行,在执行3后,instance就不是nullptr了,线程B就会直接return instance;,然而此时对象尚未构造,线程B一旦使用这个对象就会导致bug。

3. 饿汉模式

上面的两种方式,都是在第一次使用时才分配资源初始化对象,这种方式也叫懒汉模式,就像大明湖的蛤蟆,一戳一蹦跶。那么我们干脆在程序进入主函数之前就初始化,这样就绕开了线程安全的问题。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
template<typename T>
class EagerSingleton
{
private:
    static T* t_;

public:
    static T& GetInstance()
    {
        return *t_;
    }

    EagerSingleton(T&&) = delete;
    EagerSingleton(const T&) = delete;
    void operator= (const T&) = delete;

protected:
    EagerSingleton() = default;
    virtual ~EagerSingleton() = default;
};

template<typename T>
T* EagerSingleton<T>::t_ = new (std::nothrow) T;

但是这种模式也存在问题,哪怕对象不使用也会被初始化。假如对象的资源开销很大就会造成浪费。

4. C++ 11带来福音

Scott Meyers 在 《Effective C++》 的 Item 4: Make sure that objects are initialized before they’re used 里面提出了一种利用 C++ 的 static 关键字来实现的单例模式,这种实现非常简洁高效,它的特点是:

  • 仅当程序第一次执行到 GetInstance 函数时,执行 instance 对象的初始化;
  • 在 C++ 11 之后,被 static 修饰的变量可以保证是线程安全的;
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#ifndef SINGLETON_H
#define SINGLETON_H

/**
 * \brief 单例模板
 * \tparam T 需要作为单例模式使用的类型
 *
 * \code{.cpp}
 * Singleton<T>::GetInstance()
 * // OR
 * class C : public Singleton<T>
 * \endcode
 */
template<typename T>
class Singleton {
 public:
  static T &GetInstance() {
    static T instance;
    return instance;
  }

  T &operator()() const { return GetInstance(); }
  T *operator->() const { return &GetInstance(); }

  Singleton(T &&) = delete;
  Singleton(const T &) = delete;
  Singleton &operator=(const T &) = delete;

 protected:
  Singleton() = default;
  virtual ~Singleton() = default;
};

#endif // SINGLETON_H

通过禁用单例类的 copy constructor,move constructor 和 operator= 可以防止类的唯一实例被拷贝或移动;不暴露单例类的 constructor 和 destructor 可以保证单例类不会通过其他途径被实例化,同时将两者定义为 protected 可以让其被子类继承并使用。

引用