C 语言的很多库都需要在程序开头初始化,程序结束时释放资源。在 C++ 中常规的封装方法是:
module.hh
1 namespace module
2 {
3 // life time management
4 void init();
5 void free();
6
7 // operations
8 void foo();
9 void bar(int baz);
10 }
这样的 API可以和 C 语言中的对应起来,但是这种 API 极易出错,比如,有可能会在 init 前调用了 foo ,也可能在 free 后调用了 bar ,可能 init 了多次,也可能忘了 free 等等。本文将讨论这种类型的 API 在 C++ 中该怎样设计才能更安全、更 fool-proof。
(本文所提 C++ 均指 C++14 及以后版本,未说明的情况下均开启 gnu 扩展(即 -std=gnu++14),并使用 -O3 -march=native 优化,作者使用的编译器为 clang,标准库实现为 libstdc++)
要封装的 API 的特点:
-
生存期与程序相同
-
需要初始化后才能使用(init 后才能调用 foo 和 bar),且只能初始化一次
-
程序退出前要释放资源
-
C 语言实现(只有 POD)
单例模式
首先想到的是做成单例:
module.singleton.hh
1 #include <library.h>
2
3 struct non_transferable
4 {
5 non_transferable() = default;
6 non_transferable(non_transferable const&) = delete;
7 non_transferable(non_transferable &&) = delete;
8 non_transferable & operator = (non_transferable const&) = delete;
9 non_transferable & operator = (non_transferable &&) = delete;
10 };
11
12 struct module : non_transferable
13 {
14 void foo();
15 void bar(int baz);
16
17 static auto& instance()
18 {
19 static module m;
20 return m;
21 }
22
23 private:
24 module();
25 ~module();
26
27 int data;
28 library_t lib;
29 };
non_transferable 是一个用来禁止复制和移动的混入类,我们的 module 混入 non_transferable 之后就无法复制、移动了,只能通过构造函数创建实例,但是构造函数是 private 的,这样就只能从 instance 函数获得 module 类实例的引用。这样能保证 module 的实例只会构造一次(第一次调用 module::instance() 时会调用 module::module())。由于唯一存在的 module 实例是 module::instance 内的一个局部 static 变量,这样在程序退出时会正确调用析构函数(程序结束前会调用 module::~module())
优点:
-
强制了依赖关系(没有 module 实例就不能调用相关函数,module 实例只能从 instance 函数中获取)
-
用户无需考虑生存期(第一次获取实例时创建,程序结束时释放)
缺点:
-
会把成员变量暴露出来(int data)
-
如果成员变量的类型在被封装的库里(library_t 在 library.h 里),就需要把被封装库整个导入进来,而在 C++ 头文件里导入 C 语言的头文件无异于污染全局符号表(尤其是宏污染)
不那么“面向对象”
C++ 作为多范式语言,可以不必如此的“面向对象”。一个简单的改进,就是把成员变量全部从头文件移动到源文件里,变成编译单元内的全局变量(library_* 都来自于 library.h):
module.singleton.2.hh
1 struct non_transferable
2 {
3 non_transferable() = default;
4 non_transferable(non_transferable const&) = delete;
5 non_transferable(non_transferable &&) = delete;
6 non_transferable & operator = (non_transferable const&) = delete;
7 non_transferable & operator = (non_transferable &&) = delete;
8 };
9
10 struct module : non_transferable
11 {
12 void foo();
13 void bar(int baz);
14
15 static auto& instance()
16 {
17 static module m;
18 return m;
19 }
20
21 private:
22 module();
23 ~module();
24 };
module.singleton.2.cc
1 #include "module.singleton.2.hh"
2 #include <library.h>
3
4 namespace
5 {
6 int data;
7 library_t lib;
8 }
9
10 module:: module() { library_init(); }
11 module::~module() { library_free(); }
12
13 void module::foo( ) { lib = library_foo( ); }
14 void module::bar(int baz) { data = library_bar(baz); }
问题解决。
还有问题
但是如果仔细研究,还会发现一些问题。我们来写一个测试代码:
test.cc
1 #include "module.singleton.2.hh"
2
3 int main()
4 {
5 auto& m = module::instance();
6 m.foo();
7 m.bar(5);
8 }
编译一下,得到汇编代码:
test.s 主要部分
1 main:
2 .cfi_startproc
3 pushq %rax
4 .Ltmp0:
5 .cfi_def_cfa_offset 16
6 movb _ZGVZN6module8instanceEvE1m(%rip), %al # module::instance()::m
7 testb %al, %al
8 jne .LBB0_3
9 movl $_ZGVZN6module8instanceEvE1m, %edi # module::instance()::m
10 callq __cxa_guard_acquire
11 testl %eax, %eax
12 je .LBB0_3
13 movl $_ZZN6module8instanceEvE1m, %edi
14 callq _ZN6moduleC1Ev # module::module()
15 movl $_ZN6moduleD1Ev, %edi # module::~module()
16 movl $_ZZN6module8instanceEvE1m, %esi # module::instance()::m
17 movl $__dso_handle, %edx
18 callq __cxa_atexit
19 movl $_ZGVZN6module8instanceEvE1m, %edi # module::instance()::m
20 callq __cxa_guard_release
21 .LBB0_3:
22 movl $_ZZN6module8instanceEvE1m, %edi # module::instance()::m
23 callq _ZN6module3fooEv # module::foo()
24 movl $_ZZN6module8instanceEvE1m, %edi # module::instance()::m
25 movl $5, %esi # number 5
26 callq _ZN6module3barEi # module::bar()
27 xorl %eax, %eax
28 popq %rdx
29 retq
大致翻译下就是(伪代码):
1 // 汇编 6~20 行,对应于 auto& m = module::instance();,该函数调用显然已内联
2 if (!constructed[&m]) {
3 if (__cxa_guard_acquire(&m)) {
4 (&module::module)(&m);
5 __cxa_atexit(&module::~module, &m, &__dso_handle);
6 __cxa_guard_release(&m);
7 }
8 }
9
10 // 汇编 22~23 行,对应于 m.foo();
11 (&module::foo)(&m);
12
13 // 汇编 24~26 行,对应于 m.bar(5);
14 (&module::bar)(&m, 5);
可见:
-
创建 module 实例有额外的开销(要检查实例是否已经创建,要防止多个线程调用 module::instance() 时出现 race condition)
-
有冗余:foo 和 bar 不会用到 this 指针(所有成员都改成全局变量了嘛),没必要传入 &m。
改进
要去掉 this 指针,就得把成员函数做成 static 的,但是这样以后,用户可以绕过 module::instance() 直接调用 module::foo(),这样就破坏了依赖关系。所以解决方法:
module.singleton.3.hh
1 struct non_transferable
2 {
3 non_transferable() = default;
4 non_transferable(non_transferable const&) = delete;
5 non_transferable(non_transferable &&) = delete;
6 non_transferable & operator = (non_transferable const&) = delete;
7 non_transferable & operator = (non_transferable &&) = delete;
8 };
9
10 struct module : non_transferable
11 {
12 static auto& instance()
13 {
14 static module m;
15 return m;
16 }
17
18 void foo() { foo_(); }
19 void bar(int baz) { bar_(baz); };
20
21 private:
22 module();
23 ~module();
24
25 static void foo_();
26 static void bar_(int baz);
27 };
就是做一个 forwarding 函数将调用 forward 到对应的 static 函数中(foo 到 foo_,bar 到 bar_),并将 static 函数设为 private。由于内联的作用,编译器生成的代码会直接调用 foo_ 和 bar_。这样还能顺便把 const correctness 搞对。冗余问题解决。
至于调用 module::instance() 会检查示例是否已创建的开销,就只能靠用户自己解决了,一般做法就是在 main 里调用 module::instance() 然后将该 instance 的引用到处传递。
最终代码
再整理一下代码:
module.hh
1 #include <utility> // for std::forward
2
3 namespace constraint
4 {
5 struct non_transferable
6 {
7 non_transferable() = default;
8 non_transferable(non_transferable const&) = delete;
9 non_transferable(non_transferable &&) = delete;
10 non_transferable & operator = (non_transferable const&) = delete;
11 non_transferable & operator = (non_transferable &&) = delete;
12 };
13
14 template <class T>
15 struct singleton
16 {
17 using instance_type = T;
18 static auto& instance()
19 {
20 static instance_type inst;
21 return inst;
22 }
23 };
24 }
25
26 using namespace constraint;
27
28
29 #define FORWARD(NAME) 30 template <class ...ARGS> decltype(auto) NAME (ARGS&&... args) 31 { return NAME ## _ (std::forward<ARGS>(args)...); }
32
33 #define METHOD(RESULT, NAME, PARAMS...) 34 private: static RESULT NAME ## _ (PARAMS); 35 public : FORWARD(NAME)
36
37
38 struct module : non_transferable, singleton<module>
39 {
40 METHOD(void, foo);
41 METHOD(void, bar, int baz);
42
43 private:
44 friend singleton;
45 module();
46 ~module();
47 };
48
49
50 #undef METHOD
51 #undef FORWARD
module.cc
1 #include "module.hh"
2 #include <library.h>
3
4 namespace
5 {
6 int data;
7 library_t lib;
8 }
9
10 module:: module() { library_init(); }
11 module::~module() { library_free(); }
12
13 void module::foo_( ) { lib = library_foo( ); }
14 void module::bar_(int baz) { data = library_bar(baz); }
test.cc
1 #include "module.hh"
2
3 int main()
4 {
5 auto& m = module::instance();
6 m.foo();
7 m.bar(5);
8 }
总结
-
强制了依赖关系
-
用户无需考虑生存期
-
除构造外,没有任何额外开销
-
传递 module 的引用可以消除额外的构造的开销,而且传递 module 引用不会有任何开销
-
不会泄漏实现细节
-
不用在 C++ 头文件里导入 C 头文件,不会造成全局名称污染
实际使用时还得注意,要在代码外再包一个 namespace 防止自己污染全局符号表,头文件开头要加上 #pragma once 来防止多重导入。
|