C++ 全局单例封装 C 语言库 - 哆啦比猫的技术瞎扯 - Arch Linux · ドラえもん · 实时绘制
C++ 全局单例封装 C 语言库
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 来防止多重导入。
凡未特殊声明(转载/翻译),所有文章均为原创。
by Giumo Xavier Clanjor (哆啦比猫/兰威举), 2010-2019.
本作品采用知识共享署名·非商业性使用·相同方式共享 3.0 中国大陆许可协议进行许可。
文中凡未特殊声明且未声明为引用的代码均以 MIT 协议授权。