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_;
};
至此,我们可以方便的编写带有回滚机制的原子事务函数。