Cpp 单例模式
单例模式作为最简单但最常用的设计模式,整理一下 C++的相关语法吧。
单例模式简介
单例模式(Singleton Pattern),使用最广泛的设计模式之一。其意图是保证一个类仅有一个实例被构造,并提供一个访问它的全局访问接口,该实例被程序的所有模块共享。
大致的做法为:
- 定义一个单例类;
- 私有化构造函数,防止外界直接创建单例类的对象;
- 禁用拷贝构造,移动赋值等函数,可以私有化,也可以直接使用
=delete
; - 使用一个公有的静态方法获取该实例;
- 确保在第一次调用之前该实例被构造。
在 C++中我们需要考虑:
- 在什么时机构造?程序一开始,第一次调用前?
- 如何构造这个实例?在堆上构造,还是通过静态变量?如果单例在堆上构造,是否存在内存泄漏?
- 实例构造失败会如何?抛异常?返回空指针?
- 通过接口返回指针还是引用?
- 线程安全?多线程下会不会重复构造?加锁?
我们暂时不考虑类的继承问题,以及需要给单例的构造传参数的问题。 关于构造时机的不同,有以下两种习惯的称呼:
- 饿汉模式(Eager Singleton),在程序启动后立刻构造单例;
- 懒汉模式(Lazy Singleton),在第一次调用前构造单例。
单例模式实现
我们在下文中会实现多个略有不同的 Singleton
类,提供get_instance
作为接口,在实例的构造和析构时分别输出消息
1
2Singleton: call Constructor
Singleton: call Destructor
并且使用如下的 main 函数进行调用,验证构造时机并确保没有重复构造。
1
2
3
4
5
6
7
8int 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
15class 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
5Singleton: 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
15class 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
5main: 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
20static 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
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
5main: 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
16class 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
4Singleton: 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
17class 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
4main: 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
21class 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
4main: 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
21class 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
4main: begin
Singleton: call Constructor
main: hello,world
main: end