自动资源清理 - 轻量级的范围守卫实现






4.93/5 (10投票s)
2007 年 4 月 18 日
7分钟阅读

48942

358
一篇关于异常安全代码的自动资源清理的文章,介绍了一种轻量级的范围守卫实现。
- 下载源文件 - 2.7 KB
- 下载测试文件 - 4.0 KB (MSVC++ 2005)
请注意,scope_guard
依赖于 std::tr1::bind
或 boost::bind
!
引言
您一定听说过 RAII - 资源获取即初始化。这个惯用法简单地说,就是您获取/初始化一个资源,并在使用完毕后释放它。
对于 C++ 类,清理工作通常会在类的析构函数中自动完成。但 C++ 的生活并非总是那么简单——您可能会遇到 C-API,它们返回指向字符串或文件的原始指针。或者,您可能面临更复杂的情况,需要将用户对象插入内存中的向量和数据库。如果其中任何一个操作失败,您仍然希望保持一致的状态。
这不是什么新发现,但是:您是否确定资源句柄在_每种_情况下都会被触发,也就是说,不仅在方法成功执行的情况下,而且在您过早返回方法或抛出异常的错误情况下?清理工作可能非常繁琐,代码也难以阅读和理解。
背景
起初,我使用了 JaeWook Choi 的 auto mate (am::mate
) [1],我觉得它非常有用且功能强大(它具有例如条件资源清理功能)。
然后,我发现了最初由 Andrei Alexandrescu 和 Petru Marginean 开发的“作用域守卫”(scope guard),它负责在您获取资源后进行清理 [2]。请务必阅读 Alexandrescu 的文章,并查看 loki 库 [3]。也请阅读 Joshua Lehrer 的改进 [4]——在本文中,我不会讨论作用域守卫的细节,而是展示一种不同且更轻量的实现。
设计问题
栈分配
auto mate 有一个小缺点:它将您清理 `functor` 的副本存储在堆上。这意味着 auto mate 需要分配内存,而这*可能*会失败,并且不如将 `functor` 存储在栈上快。
作用域守卫将您的清理 `functor` 存储在栈上(参见注释,关于作用域守卫的创建)。
委托 `functor` 复杂性
处理 `functor` 并非易事。有许多种形式:函数对象、`static` 函数、成员函数。它们具有不同的元数(参数数量),`static` 和成员函数可能具有不同的调用约定(`__cdecl`、`__stdcall`、`__fastcall`),成员函数可能具有 const-volatile 限定。这种多样性对于作用域守卫来说,在函数对象和静态函数方面基本不是问题,因为它只需要一个特定元数的 `functor`,存储该 `functor` 并绑定其参数。对于成员函数,情况会变得更复杂,因为作用域守卫还需要对象来调用成员函数。因此,它会存储对象、成员函数及其参数。
使用 Loki,您可以通过重载了许多情况的辅助函数 `MakeGuard` 轻松创建作用域守卫。
using namespace Loki;
// guard for a static function
ScopeGuard g1 = MakeGuard(&fclose, hFile);
// guard for a member function
ScopeGuard g2 = MakeGuard(&Object::do_sg, obj);
但它并不涵盖所有可能的情况,例如,它没有为 cv 限定的成员函数或具有不同调用约定的成员函数提供重载。
随着技术发布 1 (tr1) 的到来,我们可以预计 STL 中将提供强大的 `functor` 适配器,这些适配器基于 `boost::bind`。
将作用域守卫功能与 `functor` 分离
无论如何,我认为作用域守卫的目的是在守卫超出作用域时触发一个(空元)`functor`(或者在守卫被解除时不触发)。它的职责不是处理 `functor` 的元数——即作为 `functor` 参数的绑定器——也不是区分任何 `functor` 和成员函数,也不是处理任何限定符或调用约定。
这实际上意味着我们可以将作用域守卫的真正目的与 `functor` 的创建分离开来,从而使作用域守卫的实现更加简单、易于维护,甚至更灵活。`functor` 的创建基本上就是程序员的责任。有很多方法可以从 n 元函数创建空元 `functor`……仅举几例:stl (std::bind1st
, ...)、sigc++ (sigc::bind
) 或 std::tr1::bind
(基于 boost::bind
)。
这赋予了程序员使用这些绑定功能的强大能力。
更严格的类设计
现有的一个限制是作用域守卫类不可赋值。
应该有进一步的限制:为了强制在栈上创建作用域守卫,它不能用 `new` 在堆上创建。
为了将其使用完全限制在栈上,甚至不应该允许创建指针别名。
我的方法
请注意,除了宏之外,作用域守卫功能都驻留在 `namespace` "`sg`" 中。
scope_guard_base
与 Loki::ScopeGuardImplBase
相同,并增加了上一章讨论的限制。
限制的工作方式如下
using namespace sg;
void do_sg()
{}
typedef scope_guard0<void (*)()> do_sg_resource_guard;
// can't (re)assign
do_sg_resource_guard g = make_guard(&do_sg);
g = make_guard(&do_sg);
// can't make a pointer alias
scope_guard g1 = ...;
const scope_guard_base* pg1 = &g1;
// can't create on the heap
scope_guard_base* pg = new do_sg_resource_guard(&do_sg);
这是基类 `class`
class scope_guard_base/*: nonassignable, nonheapcreatable, nonpointeraliasing*/
{
/// Copy-assignment operator is not implemented and private.
scope_guard_base& operator =(const scope_guard_base&);
/// can't create on the heap
void* operator new(std::size_t);
/// can't pointer alias
scope_guard_base* operator &();
/// can't const pointer alias
const scope_guard_base* operator &() const;
protected:
scope_guard_base() throw()
: m_dismissed()
{}
~scope_guard_base()
{}
/// Copy-ctor takes over responsibility from other scope_guard.
scope_guard_base(const scope_guard_base& other) throw()
: m_dismissed(other.m_dismissed)
{
other.dismiss();
}
template<typename T_janitor>
static void safe_execute(T_janitor& j) throw()
{
if (!j.m_dismissed)
{
try { j.execute(); }
catch(...)
{}
}
}
public:
void dismiss() const throw()
{
m_dismissed = true;
}
private:
mutable bool m_dismissed;
};
只为 nullary functor 提供一个作用域守卫
我只提供一个作用域守卫:一个 scope_guard0
(派生自 scope_guard_base
),它接受一个 nullary `functor`(无参数的 `functor`)
template<typename T_functor>
class scope_guard0: public scope_guard_base
{
public:
explicit scope_guard0(T_functor functor)
: m_functor(functor)
{}
~scope_guard0() throw()
{
scope_guard_base::safe_execute(*this);
}
void execute()
{
m_functor();
}
protected:
T_functor m_functor;
};
使用 make_guard() 创建作用域守卫
只有一个辅助函数 make_guard()
可以轻松创建上述 scope_guard
。
template<typename T_functor>
inline scope_guard0<T_functor>
make_guard(T_functor functor)
{
return scope_guard0<T_functor>(functor);
}
就是这样!
可读性问题和 MAKE_GUARD() 宏
您可能会争辩说,我的设计决策——只为 nullary `functor` 提供作用域守卫——大大降低了可读性。您之前是否写过
using namespace Loki;
ScopeGuard g = MakeGuard(&Object::do_sg, obj);
您现在必须写
using namespace sg;
scope_guard g = make_guard(boost::bind(&Object::do_sg, &obj));
// or even
scope_guard g = make_guard(boost::bind(&Object::do_sg, boost::ref(obj)));
这似乎不太直观,我同意。
我仔细考虑了这个问题,并尝试挂接到 tr1/boost bind 功能。
我的第一个尝试是为 `make_guard()` 函数提供重载,但这很快就变得复杂,因为我必须知道要提供给 `scope_guard0` 的 bind `functor` 类型。
我提出的解决方案通过一个宏扩展了作用域守卫实现,该宏使用 `std::tr1::bind` 或 `boost::bind` 创建一个 nullary `functor`,通过一些预处理器魔法传递 `functor` 及其参数,并由此创建一个守卫。
#define MAKE_GUARD(fun_n_args_tuple)\
::sg::make_guard(::boost::bind fun_n_args_tuple)
// use it like this:
scope_guard g = MAKE_GUARD((&Object::do_sg, &obj));
// or
scope_guard g = MAKE_GUARD((&ReleaseDC, hdc, NULL));
请注意 `MAKE_GUARD()` 宏的双括号!该宏只期望一个参数,即一个 n 元组,由 `functor` 及其参数组成,最终用括号括起来。
当然,您很容易忘记多余的括号,但那时编译器会发出错误的指示,因为 `MAKE_GUARD` 宏生成的代码不正确。
匿名作用域守卫
很多时候,您不需要一个命名的作用域守卫变量,因为大多数时候您不会解除守卫,也不需要一个文档记录该特定作用域守卫功能的变量名。
ON_SCOPE_EXIT()
宏 `ON_SCOPE_EXIT()` 用于此目的(类似于 Loki 的 `LOKI_ON_BLOCK_EXIT()`)。
FILE* hFile = _tfopen(_T("c:\\a_file.txt"), _T("wb"));
ON_SCOPE_EXIT((&fclose, hFile));
注意双括号!因为 `ON_SCOPE_EXIT()` 使用 `MAKE_GUARD()`,您必须将一个 n 元组传递给 `ON_SCOPE_EXIT()`。
SOME_SCOPE_GUARD
如果您不想或不能依赖 `ON_SCOPE_EXIT()`(它扩展为 `std::tr1::bind` 或 `boost::bind`),但仍然希望有一个匿名的作用域守卫变量,您可以使用宏 `SOME_SCOPE_GUARD`。
SOME_SCOPE_GUARD = make_guard(std::cout << boost::lambda::constant
("do something") << '\n');
tr1 和 boost
如果您的编译器已经提供了技术发布 1 (tr1) 中的功能,您可以在包含作用域守卫之前定义一个宏。
#define SCOPE_GUARD_HAVE_TR1_BIND
#include "scope_guard.h"
……然后 `MAKE_GUARD()` 宏将使用 `std::tr1::bind()`,否则将使用 `boost::bind()`。
需要知道的事情
-
创建作用域守卫也可能失败(就像 auto mate 一样),如果您按值绑定例如 `std::string`。
每次复制 `functor` 时,绑定的字符串也会被复制,并为每个复制的字符串分配内存。 -
Loki 的作用域守卫将 `functor` 的参数绑定为 const。由于我的实现依赖于其他绑定机制,您可能会丢失 const 绑定。
在我看来,这不是一个大问题,因为您必须知道如何使用绑定。但是,如果您犯了错误,编译器不会告诉您。例如
使用 Loki 作用域守卫时,编译器将生成一个错误。using namespace Loki; void do_sg(int& x) { --x; } int i = 5; // MakeGuard makes a ScopeGuard binding "i" as a const value. // Therefore the compiler will report an error because you can't // pass a const int to a int& ScopeGuard g = MakeGuard(&do_sg, i); // you must bind explicitly by reference: ScopeGuard g2 = MakeGuard(&do_sg, ByRef(i)); // ... after the guard fires, "i" will be 4
使用我的作用域守卫时,编译器不会告诉您。
using namespace sg; void do_sg(int& x) { --x; } int i = 5; // std::tr1::bind or boost::bind will bind "i" as a not-const value. // Therefore the compiler won't report an error scope_guard g1 = MAKE_GUARD((&do_sg, i)); // ... after the guard fires, "i" will still be 5 // you must bind explicitly by reference (std::tr1::ref or boost::ref): scope_guard g2 = MAKE_GUARD((&do_sg, ref(i))); // ... after the guard fires, "i" will be 4
-
如果您有一个成员函数用于守卫,您必须考虑如何传递对象给该成员函数。
Loki 的 `MakeGuard()` 函数和作用域守卫具有引用语义,即它们按引用接受此对象。template<...> ScopeGuard_N MakeGuard(T_return (T_obj1::*mem_fun)(), T_obj2& obj) {} // obj is passed by reference ScopeGuard g = MakeGuard(&Obj::do_sg, obj);
而使用我的实现,您依赖于 `functor` 库的功能。
`std::tr1::bind` 或 `boost::bind` 默认具有值语义,即对象按值传递!// passed by value! scope_guard g = MAKE_GUARD((&Obj::do_sg, obj)); // pass explicitly by reference, tr1::ref() or boost::ref() scope_guard g = MAKE_GUARD((&Obj::do_sg, ref(obj))); // or use a pointer scope_guard g = MAKE_GUARD((&Obj::do_sg, &obj));
-
`boost::bind` 没有用于 volatile 限定成员函数的 `bind()` 重载。
-
请注意,`std::tr1::bind` 和 `boost::bind` 也可以接受 `shared_ptr`!
class MyClass { public: void do_sg() {} }; shared_ptr<MyClass> p; ON_SCOPE_EXIT((&MyClass::do_sg, p));
-
如果您想使用 Windows API 函数,如 `ReleaseDC()` 或 `CoTaskMemFree()`,或 COM 方法,请确保在使用 boost 时定义 `#define BOOST_BIND_ENABLE_STDCALL`(对于 tr1,我现在还不知道)。
参考文献
历史
- 2007 年 4 月 18 日
- 首次发布