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);
}
基于 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
补充
单例基类
我们可以通过继承一个不可复制基类,实现一个可复用的单例模式:它自动删除了拷贝构造和赋值,并且私有化了构造函数,但是仍然需要具体写出(可带参数的)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
动态库中的单例模式
对于这里所讨论的单例模式,如果我们将其封装在动态库中使用,那么其实还会遇到很多问题,这些问题可能会导致单例并不是真正的单例,而是多个编译单元的不同实例。
这些问题可能与平台相关,可能和符号导出相关,还可能和静态库动态库混合使用相关。目前我没有精力去关注这些细节,只是将一些可能的避免措施记录一下:
- 不要将单例的实现使用inline的写法放在类的头文件中,而是在头文件中只进行声明,在单独的cpp文件中进行实现;
- 对于一个大的系统,将所有的单例集中到一个动态库模块中,避免分散。
- 对于跨动态库使用单例,注意明确标记当前应该是导入还是导出符号。
CRTP
奇异递归模板模式 CRTP 是一个非常经典的C++静态多态实现方案,在很多库中都有应用,这里整理一下。
概述
奇异递归模板模式(Curiously Recurring Template Pattern, CRTP)是 C++ 编程中一种常用的静态多态方案,它通过模板继承和静态绑定来实现类型多态。 相比于C++直接提供的基于虚函数的动态多态方案,CRTP既可以避免虚函数的额外性能开销,还可以在编译时获得更好的类型检查和优化。
CRTP的核心思想如下:
- 模板基类是一个模板类,模板参数是派生类自身。(这种递归的模板参数是这个模式名字的来源)
- 派生类:派生类继承自通过自身类型实例化的基类。
通过基类调用方法时,首先会将this指针进行类型转换为派生类的类型(因为派生类类型是模板参数,基类可以获取到派生类类型),然后就可以调用派生类的方法。
这种处理导致我们无法添加基类自身的方法实现并使用,因此基类只能作为一个抽象基类使用。
这一切都发生在模板实例化的过程中,在编译期中即可完全确定。
CRTP具有如下的优势:
- 静态多态:CRTP的是静态多态,通过基类调用派生类的方法是静态绑定的,完全在编译期通过模板实例化确定。与动态多态相比效率更高,因为完全避免了虚函数调用的开销。
- 类型安全:CRTP在编译时可以进行类型检查,从而确保方法调用的类型安全。
事实上,除了原理不同,CRTP和虚函数的使用情景并不是完全重合的,例如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
38
39
40
41
42
43
44
45
46
47
48
class Base {
public:
virtual ~Base() = default;
virtual void run() const { std::cout << "Base Run\n"; }
};
class Derived1 : public Base {
private:
std::string m_name;
public:
explicit Derived1(std::string name) : m_name(std::move(name)) {}
void run() const override {
std::cout << "Derived1 Run, his name is " << m_name << '\n';
}
};
class Derived2 : public Base {
private:
std::string m_name;
public:
explicit Derived2(std::string name) : m_name(std::move(name)) {}
void run() const override {
std::cout << "Derived2 Run, her name is " << m_name << '\n';
}
};
void Action(Base &obj) { obj.run(); }
int main() {
Base b;
Action(b);
Derived1 d1("Tom");
Action(d1);
Derived2 d2("Jerry");
Action(d2);
return 0;
}
运行结果如下 1
2
3Base Run
Derived1 Run, his name is Tom
Derived2 Run, her name is Jerry
其中的Action函数中,我们通过基类的引用(或指针)调用了虚函数,实现了动态多态
1
2
3void Action(Base &obj) {
obj.run();
}
在CRTP的实现中,我们将派生类作为模板参数传递给基类,通过基类调用时,首先通过类型转换到派生类,然后调用对应的方法。
1
2
3
4
5template <typename Derived>
class Base {
public:
void run() { static_cast<Derived *>(this)->run(); }
};
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
38
39
40
41
42
43
44
45
46
template <typename Derived>
class Base {
public:
void run() { static_cast<Derived *>(this)->run(); }
};
class Derived1 : public Base<Derived1> {
private:
std::string m_name;
public:
explicit Derived1(std::string name) : m_name(std::move(name)) {}
void run() const {
std::cout << "Derived1 Run, his name is " << m_name << '\n';
}
};
class Derived2 : public Base<Derived2> {
private:
std::string m_name;
public:
explicit Derived2(std::string name) : m_name(std::move(name)) {}
void run() const {
std::cout << "Derived2 Run, her name is " << m_name << '\n';
}
};
template <typename T>
void Action(Base<T> &obj) {
obj.run();
}
int main() {
Derived1 d1("Tom");
Action(d1);
Derived2 d2("Jerry");
Action(d2);
return 0;
}
运行结果如下 1
2Derived1 Run, his name is Tom
Derived2 Run, her name is Jerry
注意到这里我们无法实例化和调用基类对象,基类只是起到抽象基类的作用。
有时为了省略频繁出现的static_cast,也可以在类中提供derived方法
1
2
3
4
5
6constexpr const Derived &derived() const {
return static_cast<const Derived &>(*this);
}
constexpr Derived &derived(){
return static_cast<Derived &>(*this);
}
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// 第一层:基类 A
template <typename D>
class A {
public:
void run() {
static_cast<D*>(this)->func1();
static_cast<D*>(this)->func2();
static_cast<D*>(this)->func3();
}
void func1() {
std::cout << "A::func1\n";
}
void func2() {
std::cout << "A::func2\n";
}
void func3() {
std::cout << "A::func3\n";
}
};
// 第二层:中间类 B
template <typename D>
class B : public A<D> {
public:
void func3() {
std::cout << "B::func3\n";
static_cast<D*>(this)->func4();
}
void func4() {
std::cout << "B::func4\n";
}
};
// 第三层:具体类 C1
class C1 : public B<C1> {
public:
void func4() {
std::cout << "C1::func4\n";
}
};
// 第三层:具体类 C2
class C2 : public B<C2> {
public:
void func2() {
std::cout << "C2::func2\n";
}
void func4() {
std::cout << "C2::func4\n";
}
};
注意中间基类在定义时仍然传递的是模板类型参数D,并没有将自身传入,只有C1和C2因为在定义时传入了自身,因此可以实例化。
如果将中间类改成嵌套的写法 1
2
3
4
5
6
7
8
9
10
11
12
13// 第二层:中间类 B
template <typename D>
class B : public A<B<D>> {
public:
void func3() {
std::cout << "B::func3\n";
static_cast<D*>(this)->func4();
}
void func4() {
std::cout << "B::func4\n";
}
};
可能有意料之外的行为,因为类模板的递归太复杂了,懒得去考究。
Pimpl
Pimpl(Pointer to Implementation)是一种常用的C++设计模式,用于隐藏类的实现细节、减少编译依赖性和提高封装性。
概述
Pimpl模式主要的思路就是:在实现类的外层套上一层简单的接口类,接口类只含有一个指向实现类的指针,并暴露必要的接口。 注意接口类包含的不是实现类对象,而是实现类指针,否则修改实现类无法做到二进制的稳定性。
Pimpl模式可以达到如下的效果:
- 隐藏实现细节:实现细节被封装在实现类中,提供给用户的接口类只暴露必要的接口,提高代码的封装性
- 维护接口稳定性:我们只需要维护暴露在接口类中的接口稳定性即可,实现类的内部可以自由地进行更改
- 减少编译依赖性:由于接口类的实现细节被隐藏,接口类只含有实现类的指针,对实现类进行的细节改变不会导致依赖于接口类的文件重新编译,从而减少编译时间。
Pimpl模式和继承以及虚函数的作用是不重合的,并不存在相互取代的关系。 和虚函数类似,Pimpl增加了间接的指针调用,这必然会影响到程序的运行效率。
示例
包括三个文件:
Demo.h:接口类的声明Demo.cpp:实现类的完整实现,以及接口类的实现main.cpp:使用接口类
1 | // Demo.h |
运行结果 1
call DemoImpl temp run()
