当 Windows API 遇上 RAII
什么是 RAII (Resource Acquisition Is Initialization) ?
RAII (Resource Acquisition Is Initialization), 也称为”资源获取就是初始化”, 是 C++ 语言的一种管理资源, 避免泄漏的惯用法. C++ 标准保证任何情况下, 已构造的对象最终会销毁, 即它的析构函数最终会被调用. 简单的说, RAII 的做法是使用一个对象, 在其构造时获取资源, 在对象生命期控制对资源的访问使之始终保持有效, 最后在对象析构的时候释放资源.
RAII 是保证代码异常安全的重要基础设施. RAII 的使用场景有很多, 如: C++11 中的智能指针, scope lock, scope exit 等等. (早在2000年,Andrei Alexandrescu 就在DDJ杂志上发表了一篇文章,提出了这个叫做 ScopeGuard 的设施)
当 Windows API 遇上 RAII
Windows API 大多是 C 语言风格的函数和句柄, 或者是 COM 风格的接口, 这些用起来都不太方便, 需要进行一定的封装. 至于为什么要封装就不用多说了, 如果你想要异常安全, 想要不必在每个分支中都写清理代码的话, 你一定知道利用 RAII 封装的意义.
ATL 中有对 COM 接口的封装, 智能指针 CComPtr
, CComQIPtr
解决了一遍遍的手工 Release
以及 QueryInterface
. 但对于普通的 C 语言风格的函数和句柄呢? 难道还要一遍遍的 CloseHandle
, ReleaseDC
, GlobalUnlock
麽? 弱爆了.
借助 ScopeGuard
和 lambda 表达式(⊙_⊙)? 可以是可以, 但是并不是所有的资源获取都会成功, 那么每次都要产生一个具名的 ScopeGuard
, 在申请失败的时候调用 Dismiss
, 取消清理的动作嘛? 像这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
这样如果连续申请多个资源 ScopeGuard
对象命名都会成为问题. 又或者是先判断资源是否申请成功, 然后再使用匿名的 ScopeGuard
来保证正确释放资源? 像这样:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
这样好是好, 可是割裂了申请与释放的代码, 而且每申请一个资源就要至少写上 3 行或以上结构类似代码(可以考虑宏了) ?
当然, 我们是有追求的, 我们还需要更厉害的设施. 也许 unique_ptr
可以给我提供一些思路. 我们需要一个基本的类型, 也许是 HANDLE
, 也许是 HINTERNET
等等, 同时我们还需要一个清理函数. 再加上一个资源是否可用的接口即可. 为了避免过多的模板参数, 我们把清理函数以及不可用资源封装到 Traits
类中, 下面是一个例子, 可以很好的完成我们的需求. 另外仿照 unique_ptr
加了一些 move 语义, 转移构造等东西. 下面的代码只实现了 HANDLE
的特化版本, 相信 HINTERNET
的版本, 大家写起来应该也是毫无压力了. 只需要写 Traits
类即可.
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 |
|
使用起来应该是这样的:
1 2 3 4 5 6 7 |
|
int i = socket;
这一句很有意思, 我们为了让它能够编译失败, 费了不少功夫. 内部类 bool_struct
就是完全为它而准备. 这也是为什么我们不直接提供 operator bool
的原因. 私有化 operator==
和 operator!=
是为了禁止两个资源进行比较. 而使用内部类成员指针就是为了提供 operator bool
类似功能的同时, 避免它能够提升为 int
等类型. 当然如果我们直接提供一个 is_valid
成员函数, 而不提供隐身转换, 那么就不会有这么多的问题了.
看起来还差错误处理的内容, 不过都到这个份上了, 错误处理就不是问题了吧. 我们可以写各种 check
函数的重载版本, 当 check
失败时抛出异常. 这样就大功告成了.