C++ 全局单例封装 C 语言库 - 哆啦比猫's Blog - I'm an ArchLinuxer

C++ 全局单例封装 C 语言库

哆啦比猫 posted @ 2015年3月16日 16:19 in C/C++ with tags c++ singleton global c wrapper , 1908 阅读

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 的特点:

  1. 生存期与程序相同
  2. 需要初始化后才能使用(init 后才能调用 foo 和 bar),且只能初始化一次
  3. 程序退出前要释放资源
  4. 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, 2011, 2012, 2013, 2014, 2015-2016 and 2017.
知识共享许可协议本作品采用知识共享署名·非商业性使用·相同方式共享 3.0 中国大陆许可协议进行许可。
文中凡未特殊声明且未声明为引用的代码均以 MIT 协议授权。

blog comments powered by Disqus
© 2010, 2011, 2012, 2013, 2014, 2015-2016 and 2017 Giumo Xavier Clanjor (哆啦比猫/兰威举).
© 2013, 2014, 2015-2016 and 2017 The Dark Colorscheme Designed by Giumo Xavier Clanjor (哆啦比猫/兰威举).
知识共享署名·非商业性使用·相同方式共享 3.0 中国大陆许可协议
| © 2007 LinuxGem | Design by Matthew "Agent Spork" McGee