65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (10投票s)

2007 年 4 月 18 日

7分钟阅读

viewsIcon

48942

downloadIcon

358

一篇关于异常安全代码的自动资源清理的文章,介绍了一种轻量级的范围守卫实现。

请注意,scope_guard 依赖于 std::tr1::bindboost::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_baseLoki::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,我现在还不知道)。

参考文献

  • [1] JaeWook Choi:以通用方式编写异常安全代码
  • [2] Andrei 和 Petru 的原始文章
  • [3] Loki::ScopeGuard
  • [4] Joshua Lehrer 的描述

历史

  • 2007 年 4 月 18 日
    • 首次发布
© . All rights reserved.