Scope Guard

Scope Guard

C++ 原子事务 #

本文所介绍的ScopeGuard方法来自于Andrei-Alexandrescu 和 Pertu-Marginean在2000年发布的一篇文章。其提出了一种更加安全和优美的原子事物处理机制。

原子事务是指,一个或一组动作,要么全做,要么全不做。在代码中,体现为操作失败时的回滚操作。

比较直观的做法是在代码中引入大量的异常处理。这不仅让代码变得更长而且更加不易扩展,当添加一个新的操作将会引入大量的异常处理代码。

另外一种做法则是遵循“构造时创建资源,析构时清除资源”的原则来实现,需要为每一种操作实现一个单独的操作类,大致如下:

class VectorInserter{

public:

    VectorInserter(std::vector& v, User& u): container_(v), commit_(false) {
        container_.push_back(&u);
    }

    void Commit() throw(){
        commit_ = true;
    }

    ~VectorInserter() {
        if (!commit_) container_.pop_back();
    }

private:

    std::vector& container_;

    bool commit_;

};

你可以这样来使用这个操作类:


void User::AddFriend(User& newFriend)  {

    VectorInserter ins(friends_, &newFriend);

    pDB_->AddFriend(GetName(), newFriend.GetName());

    // Everything went fine, commit the vector insertion

    ins.Commit();

}

可以看到,在析构函数中,只有当commit没有被调用的时候,才会进行回滚操作。这样做的好处是,避免编写大量的异常处理代码,一旦操作成功就设置commit为true,否则在析构函数中进行回滚操作。

Scope Guard 的实现方法 #

以上的思路缺点在于需要编写大量的操作类。使用Scope Guard可以写出如下简洁的代码。

void User::AddFriend(User& newFriend) {
   friends_.push_back(&newFriend);
   ScopeGuard guard = MakeObjGuard( friends_,  &UserCont::pop_back);
   pDB_->AddFriend(GetName(), newFriend.GetName());
   guard.Dismiss();
}

代码中的guard唯一的作用就是在函数结束的时候调用pop_back, 除非已经调用过Dismiss函数。

对于一组操作,我们可能需要多个guard来针对每个操作进行回滚。


// 调用某个成员函数
friends_.push_back(&newFriend);
ScopeGuard guard = MakeObjGuard(friends_, &UserCont::pop_back);

// ScopeGuard也可用于普通函数:
void* buffer = std::malloc(1024);
ScopeGuard freeIt = MakeGuard(std::free, buffer);
FILE* topSecret = std::fopen("cia.txt");
ScopeGuard closeIt = MakeGuard(std::fclose, topSecret);

实现 Scope Guard #

首先来实现一个基类:

class ScopeGuardImplBase {
public:

   void Dismiss() const throw() {
       dismissed_ = true;    
   }

protected:
   
   ScopeGuardImplBase() : dismissed_(false) {}
   ScopeGuardImplBase(const ScopeGuardImplBase& other) : dismissed_(other.dismissed_){    
       other.Dismiss(); 
   }
   
   ~ScopeGuardImplBase() {} // nonvirtual (see below why)
   
   mutable bool dismissed_;

private:
   
   // Disable assignment
   ScopeGuardImplBase& operator=(const ScopeGuardImplBase&);
};

由于回滚函数的不同,所以我们需要不同的子类来具体的实现如何回滚。具体的包括调用有参函数、无参函数、成员函数等。

下面以有一个参数的函数为例:


template <typename Fun, typename Parm>
class ScopeGuardImpl1 : public ScopeGuardImplBase {

public:

    ScopeGuardImpl1(const Fun& fun, const Parm& parm): fun_(fun), parm_(parm) {}

    ~ScopeGuardImpl1() {
        if (!dismissed_) fun_(parm_);
    }

private:

    Fun fun_;

    const Parm parm_;

};

并实现一个工厂方法:


template <typename Fun, typename Parm>
ScopeGuardImpl1<Fun, Parm> MakeGuard(const Fun& fun, const Parm& parm) {
   return ScopeGuardImpl1<Fun, Parm>(fun, parm);
}

有了以上的代码,我们就可以使用这个ScopeGuardImpl1的对象来进行回滚。但是这并不利于我们的代码,对于调用者来说,他需要区分不同类型的回滚操作,并实例化不同的对象。因此我们使用一个typedef来整理一下:


typedef const ScopeGuardImplBase& ScopeGuard;

将基类的常量引用重定义为ScopeGuard。

进一步复制完善 #

接下来我们就可以实现不同类型的对象和重载工厂函数。

  • 无参函数

template <typename Fun>
class ScopeGuardImpl0 : public ScopeGuardImplBase {

public:

    ScopeGuardImpl0(const Fun& fun): fun_(fun) {}

    ~ScopeGuardImpl0() {
        if (!dismissed_) fun_();
    }

private:

    Fun fun_;

};



template <typename Fun>
ScopeGuardImpl0<Fun> MakeGuard(const Fun& fun) {
   return ScopeGuardImpl0<Fun >(fun);
}
  • 带参数的成员函数
template <class Obj, typename MemFun>
class ObjScopeGuardImpl0 : public ScopeGuardImplBase {
public:

   ObjScopeGuardImpl0(Obj& obj, MemFun memFun): obj_(obj), memFun_(memFun) {}

   ~ObjScopeGuardImpl0() {
       if (!dismissed_) (obj_.*fun_)();
   }
   
private:

   Obj& obj_;
   MemFun memFun_;
};


template <class Obj, typename MemFun>
ObjScopeGuardImpl0<Obj, MemFun, Parm> MakeObjGuard(Obj& obj, Fun fun) {
   return ObjScopeGuardImpl0<Obj, MemFun>(obj, fun);
}


// 创建方法
// ScopeGuard guard = MakeObjGuard(friends_, &UserCont::pop_back);

进一步优化(异常处理) #

以上的实现有一个新的问题,因为我们要在析构函数里调用一个外部传入的函数,而如果在析构函数中抛出异常非常危险,由于有栈展开的机制存在,将会引发程序的崩溃。

关于栈展开的解释和过程可以参考:https://docs.microsoft.com/zh-cn/cpp/cpp/exceptions-and-stack-unwinding-in-cpp?view=msvc-170

因此我们坚决防止析构函数抛出异常。在代码中我们需要对外部函数的调用进行异常捕获。并不做任何处理。


...

~ScopeGuardImp0() {

     if (!dismissed_)
        try { (obj_.*fun_)(); }
        catch(...) {}

}

进一步优化(传递引用) #

如果我们希望在回滚函数中传递一个引用值,除了新编写一组新的子类来支持,也可以使用一个工具类来转换引用。


template <class T>
class RefHolder {
   
    T& ref_;

public:
    // 通过一个拷贝构造,拷贝出一份新的引用。
   RefHolder(T& ref) : ref_(ref) {}
   operator T& () const {
       return ref_;
   }
};


template <class T>
inline RefHolder<T> ByRef(T& t) {
   return RefHolder<T>(t);
}

使用时对于需要被引用的参数,可以被包装一下。

// 回滚函数, 修改引用参数
void Decrement(int& x) { --x; }

void UseResource(int refCount) {
   ++refCount;

   ScopeGuard guard = MakeGuard(Decrement, ByRef(refCount));
   ...
}

使用上面的方法,可以将传递引用通过拷贝值的方式,实现实际上传递的还是引用。

并且我们可以对ScopeGuardImpl1中的参数param使用const进行修饰。使用这种方式,可以在编译阶段进行安全检查。


template <typename Fun, typename Parm>
class ScopeGuardImpl1 : public ScopeGuardImplBase {
   ...
private:
   Fun fun_;
   const Parm parm_;
};

至此,我们可以方便的编写带有回滚机制的原子事务函数。