单例模式作为最简单但最常用的设计模式,整理一下 C++的相关语法吧。

单例模式简介

单例模式(Singleton Pattern),使用最广泛的设计模式之一。其意图是保证一个类仅有一个实例被构造,并提供一个访问它的全局访问接口,该实例被程序的所有模块共享。

大致的做法为:

  1. 定义一个单例类;
  2. 私有化构造函数,防止外界直接创建单例类的对象;
  3. 禁用拷贝构造,移动赋值等函数,可以私有化,也可以直接使用=delete
  4. 使用一个公有的静态方法获取该实例;
  5. 确保在第一次调用之前该实例被构造。

在 C++中我们需要考虑:

  1. 在什么时机构造?程序一开始,第一次调用前?
  2. 如何构造这个实例?在堆上构造,还是通过静态变量?如果单例在堆上构造,是否存在内存泄漏?
  3. 实例构造失败会如何?抛异常?返回空指针?
  4. 通过接口返回指针还是引用?
  5. 线程安全?多线程下会不会重复构造?加锁?

我们暂时不考虑类的继承问题,以及需要给单例的构造传参数的问题。 关于构造时机的不同,有以下两种习惯的称呼:

  • 饿汉模式(Eager Singleton),在程序启动后立刻构造单例;
  • 懒汉模式(Lazy Singleton),在第一次调用前构造单例。

单例模式实现

我们在下文中会实现多个略有不同的 Singleton 类,提供get_instance作为接口,在实例的构造和析构时分别输出消息

1
2
Singleton: call Constructor
Singleton: call Destructor

并且使用如下的 main 函数进行调用,验证构造时机并确保没有重复构造。

1
2
3
4
5
6
7
8
int main(int argc, char *argv[]) {
std::cout << "main: begin\n";
Singleton::get_instance();
std::cout << "main: hello,world\n";
Singleton::get_instance();
std::cout << "main: end\n";
return 0;
}

这里我们把做法分成两类,然后进行进一步的讨论:

  • 第一类通过静态变量实现;
  • 第二类需要在堆上构造单例(更加复杂,存在更多的问题)。

基于静态变量实现

已知类的静态变量的构造是在程序启动后,main 函数之前完成的,而函数体内部的局部静态变量则是在第一次调用函数时完成的,这两个特点刚好可以分别满足我们的需求。

由于是通过静态变量实现的单例,我们不需要考虑内存泄漏的问题,构造失败也直接通过抛异常体现,我们的接口返回静态变量的引用即可。C++保证静态变量的构造是线程安全的,从 C++11 开始,保证局部静态变量的构造也是线程安全的,这些是编译器自动完成的,我们不需要考虑。

饿汉版

基于类的静态变量,实现饿汉版的单例模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Singleton {
protected:
Singleton() { std::cout << "Singleton: call Constructor\n"; };

static Singleton demo; // declare
public:
Singleton(const Singleton &) = delete;
Singleton &operator=(const Singleton &) = delete;

virtual ~Singleton() { std::cout << "Singleton: call Destructor\n"; }

static Singleton &get_instance() { return demo; }
};

Singleton Singleton::demo; // define

运行结果:(确实在 main 函数之前构造,而且只构造了一次)

1
2
3
4
5
Singleton: call Constructor
main: begin
main: hello,world
main: end
Singleton: call Destructor

懒汉版

基于类的静态函数的局部静态变量,实现懒汉版的单例模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Singleton {
protected:
Singleton() { std::cout << "Singleton: call Constructor\n"; };

public:
Singleton(const Singleton &) = delete;
Singleton &operator=(const Singleton &) = delete;

virtual ~Singleton() { std::cout << "Singleton: call Destructor\n"; }

static Singleton &get_instance() {
static Singleton demo;
return demo;
}
};

运行结果:(确实在第一次调用时构造,而且只构造了一次)

1
2
3
4
5
main: begin
Singleton: call Constructor
main: hello,world
main: end
Singleton: call Destructor

这个实现在语法上就漂亮了很多。事实上,这就是在 C++11 之后,Effective C++最推荐的单例模式写法。

C++11 保证局部静态变量的构造是线程安全的,我们可以通过cppinsight.io查看经过简单处理之后的更本质的形式,上文懒汉版单例类的get_instance可能被处理为如下形式(具体实现与编译器有关)。由此可见,编译器确实做了一些确保构造安全和线程安全的工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static inline Singleton &get_instance() {
static uint64_t __demoGuard;
alignas(Singleton) static char __demo[sizeof(Singleton)];

if (!__demoGuard) {
if (__cxa_guard_acquire(&__demoGuard)) {
try {
new (&__demo) Singleton();
__demoGuard = true;
}
catch (...) {
__cxa_guard_abort(&__demoGuard);
throw;
}

__cxa_guard_release(&__demoGuard);
}
}
return *reinterpret_cast<Singleton *>(__demo);
}

单例基类

我们可以通过继承一个不可复制基类,实现一个可复用的单例模式:它自动删除了拷贝构造和赋值,并且私有化了构造函数,但是仍然需要具体写出(可带参数的)get_instance接口,这里没有选择 CRTP 等技巧通过模板类实现,因为那样的实例化可能不受控制,无法禁止某些非法语法。

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
35
36
37
#include <iostream>

class SingletonBase {
protected:
SingletonBase() = default;

public:
SingletonBase(const SingletonBase &) = delete;
SingletonBase &operator=(const SingletonBase &) = delete;
~SingletonBase() = default;
};

class A : public SingletonBase {
private:
int m_s{0};

A(int s) : m_s(s) {
std::cout << "A: call Constructor with s=" << m_s << "\n";
}

~A() { std::cout << "A: call Destructor with s=" << m_s << "\n"; }

public:
static A &get_instance(int s) {
static A tmp(s);
return tmp;
}
};

int main(int argc, char *argv[]) {
std::cout << "main: begin\n";
A::get_instance(4);
std::cout << "main: hello,world\n";
A::get_instance(40);
std::cout << "main: end\n";
return 0;
}

运行结果如下:

1
2
3
4
5
main: begin
A: call Constructor with s=4
main: hello,world
main: end
A: call Destructor with s=4

基于 new 实现

如果不使用静态变量,那么我们就需要直接面对构造安全、线程安全和内存安全的问题,这通常是吃力不讨好的行为,但是鉴于单例模式有很多类似的实现,还是记录一下吧。

饿汉版

这里还是使用了类的静态成员,虽然是指针类型,仍需要在类的定义之外进行 new,因为必须在类的定义完成之后。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Singleton {
protected:
Singleton() { std::cout << "Singleton: call Constructor\n"; };

static Singleton *demo;

public:
Singleton(const Singleton &) = delete;
Singleton &operator=(const Singleton &) = delete;

virtual ~Singleton() { std::cout << "Singleton: call Destructor\n"; }

static Singleton *get_instance() { return demo; }
};

Singleton *Singleton::demo = new Singleton();

运行结果:(确实在 main 函数之前构造,而且只构造了一次,注意这里没有调用析构函数)

1
2
3
4
Singleton: call Constructor
main: begin
main: hello,world
main: end

懒汉版

这里还是使用了类的静态成员,作为指针类型可以 inline 初始化为 nullptr,不需要在类的外面加代码了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Singleton {
protected:
Singleton() { std::cout << "Singleton: call Constructor\n"; };

inline static Singleton *demo{nullptr};

public:
Singleton(const Singleton &) = delete;
Singleton &operator=(const Singleton &) = delete;

virtual ~Singleton() { std::cout << "Singleton: call Destructor\n"; }

static Singleton *get_instance() {
if (demo == nullptr) { demo = new Singleton(); }
return demo;
}
};

运行结果:(确实在第一次调用之前构造,而且只构造了一次,注意这里没有调用析构函数)

1
2
3
4
main: begin
Singleton: call Constructor
main: hello,world
main: end

问题梳理

这两个原始版本的基于 new 的单例模式,存在非常多的问题!

  • 第一个问题是单例的析构函数始终不会被调用,直到程序结束回收内存。解决方法可以是 RAII,直接使用 C++的智能指针,或者自己实现一个嵌套类,在类的静态成员析构时调用 delete,这里略去。
  • 第二个问题是构造失败的处理,比如 new 失败了?这只是细节末节,加一些保护措施即可,不做讨论。
  • 第三个问题是线程安全,我们只需要考虑饿汉版本,因为全局变量的构造是线程安全的。

我们重点关注第三个问题,针对线程安全进行改进。

饿汉版二

这里采用两次判断:如果是空,先加锁,如果是空,才构造。这种写法曾经被广泛采用,称为双检测锁模式(DCL: Double-Checked Locking Pattern),这样的做法可以很大程度上确保线性安全。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Singleton {
protected:
Singleton() { std::cout << "Singleton: call Constructor\n"; };

inline static Singleton *demo{nullptr};
inline static std::mutex lock;

public:
Singleton(const Singleton &) = delete;
Singleton &operator=(const Singleton &) = delete;

virtual ~Singleton() { std::cout << "Singleton: call Destructor\n"; }

static Singleton *get_instance() {
if (demo == nullptr) {
std::lock_guard tmp{lock};
if (demo == nullptr) { demo = new Singleton(); }
}
return demo;
}
};

运行结果:(确实在第一次调用之前构造,而且只构造了一次,注意这里没有调用析构函数)

1
2
3
4
main: begin
Singleton: call Constructor
main: hello,world
main: end

饿汉版三

DCL 的写法还是可能有问题的,在特定情况下可能出现内存操作在编译器优化后的顺序冲突问题,这里不做讨论,只是给出解决办法:C++11 之后使用 atomic 原子操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Singleton {
protected:
Singleton() { std::cout << "Singleton: call Constructor\n"; };

inline static std::atomic<Singleton *> demo{nullptr};
inline static std::mutex lock;

public:
Singleton(const Singleton &) = delete;
Singleton &operator=(const Singleton &) = delete;

virtual ~Singleton() { std::cout << "Singleton: call Destructor\n"; }

static Singleton *get_instance() {
if (demo == nullptr) {
std::lock_guard lc{lock};
if (demo == nullptr) { demo = new Singleton(); }
}
return demo;
}
};

运行结果:(确实在第一次调用之前构造,而且只构造了一次,注意这里没有调用析构函数)

1
2
3
4
main: begin
Singleton: call Constructor
main: hello,world
main: end