c mysql代码中写事务_c 中如何实现事务

方法4:Petru的方法

用ScopeGuard——我们稍后详细介绍——你很容易就可以写出简洁、正确而高效的代码:

void User::AddFriend(User& newFriend)

{

friends_.push_back(&newFriend);

ScopeGuard guard = MakeObjGuard(

friends_,& UserCont::pop_back);

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

guard.Dismiss();

}

上面代码里,guard对象唯一的任务就是在它离开作用域时,调用friends_.pop_back,除非你调用了Dismiss。如果你调用了,那么guard就什么也不做。

ScopeGuard在它的析构函数里实现自动调用某个全局函数或者成员函数。在有异常的情况下,你会想要实现自动撤销原子操作的功能,这时候ScopeGuard会很有用。

你可以这样使用ScopeGuard:如果你希望几个操作按照“要么全做,要么全不做”的方式工作,你可以在紧接着每个操作后面放一个ScopeGuard,这个ScopeGuard可以取消前面的操作:

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);

当整个原子操作成功时,你Dismiss所有guard对象。否则每个ScopeGuard对象会忠实的调用你构造它时所传的那个函数。

有了ScopeGuard,你可以简单的安置各种撤销操作,而不再需要写特别的类来做诸如删除vector的最后一个元素、释放内存、关闭文件这样的事情。这使ScopeGuard成为编写异常安全代码的一个极其有用、并且可重用的解决方案,它使一切变得很简单。

实现ScopeGuard

ScopeGuard是对C++惯用法RAII(资源分配即初始化)典型实现的一个推广。它们的区别在于ScopeGuard只关注资源清理的那部分——资源分配由你自己做,而ScopeGuard处理资源的释放(事实上,可以论证清理工作是这个谚语里最重要的部分)。

释放资源有很多种形式,比如调用一个函数、调用一个functor、或者调用一个对象的成员函数,而每种方式都可能有零个、一个或者更多的参数。

自然,我们通过一个类层次关系来对这些变体建模。层次中各个类的对象的析构函数完成实际工作。层次中的根为ScopeGuardImplBase类,如下:

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&);

};

ScopeGuardImplBase集中了对dismissed_标志的管理,这个标志控制派生类是否要执行清理工作。如果dismissed_为真,则派生类在他们的析构函数里什么也不做。

现在我们来看看ScopeGuardImplBase析构函数定义里缺少的virtual。如果析构函数不是virtual的,你怎么可以期望析构函数有正确的多态行为呢?好,把你的好奇心再保持一会儿,我们手里还有张王牌,我们可以通过它得到多态的析构行为,而不必付出虚函数的代价。

现在我们先来看看怎么实现这样一个对象,它在析构函数里调用一个带一个参数的函数或者functor。然而当你调用了Dismiss,那么这个函数或者functor就不会被调用。

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_;

};

为了方便使用ScopeGuardImpl1,我们写一个辅助函数。

template< typename Fun, typename Parm>

ScopeGuardImpl1MakeGuard(const Fun& fun, const Parm& parm)

{

return ScopeGuardImpl1(fun, parm);

}

MakeGuard 依靠编译器推导出模板函数中的模板参数,这样你就不用自己指定ScopeGuardImpl1的模板参数了——事实上你不需要显式创建 ScopeGuardImpl1对象。这个技巧也被一些标准库中的函数所使用,如make_pair和bind1st。

你还对不使用虚构造函数而得到多态性析构行为的方法感到好奇吗?下面是ScopeGuard的定义,会让你大吃一惊的是,它仅仅是一个typedef:

typedef const ScopeGuardImplBase& ScopeGuard;

好了现在让我们来揭开全部神秘机制。根据C++标准,如果const的引用被初始化为对一个临时变量的引用,那么它会使这个临时变量的生命期变得和它自己一样。让我们举个例子来解释这件事。如果你写:

FILE* topSecret = std::fopen('cia.txt');

ScopeGuard closeIt = MakeGuard(std::fclose, topSecret);

那么MakeGuard创建了一个临时变量,它的类型为(看以前做一下深呼吸):

ScopeGuardImpl1

这是因为std::fclose是接受FILE*类型参数返回int的函数。具有上面那个类型的临时变量被指派给了const引用closeIt。根据上面提到的C++语言规则,这个临时变量会和它的引用closeIt有同样长的生存期——当这个临时变量被析构时,会调用正确的析构函数。接着,析构函数关闭文件。

ScopeGuardImpl1支持有带参数的函数(或functor)。很容易就可以写出不带参数、带两个参数或带更多参数的类(ScopeGuardImpl0、ScopeGuardImpl2……)。当你有了这些类,你就可以重载MakeGuard,从而得到一个优美、统一的语法:

template< typename Fun>

ScopeGuardImpl0MakeGuard(const Fun& fun)

{

return ScopeGuardImpl0(fun);

}

...

到现在为止,我们已经有了一个强大的工具来表达调用一组函数的原子操作。MakeGuard是一个优秀的工具,特别是它同样可以用于C语言的API,而不需要写很多包装类。

更好的是,它不损失效率,因为它不涉及到虚函数调用。

针对对象和成员函数的ScopeGuard

到现在为止,一切都很好,但是怎么调用对象的成员函数呢?其实这一点也不难。让我们来实现ObjScopeGuardImpl0,一个可以调用对象的无参数成员函数的类模板。

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_;

};

ObjScopeGuardImpl0有一点特别,因为它用了不太为人所知的语法:指向成员函数的指针和operator.*()。为了理解它是如何工作的,让我们来看看MakeObjGuard的实现(我们在本节开始已经利用过MakeObjGuard了)。

template< class Obj, typename MemFun>

ObjScopeGuardImpl0MakeObjGuard(Obj& obj, Fun fun)

{

return ObjScopeGuardImpl0(obj, fun);

}

现在,如果你调用:

ScopeGuard guard = MakeObjGuard(friends_,& UserCont::pop_back);

会创建一个如下类型的对象:

ObjScopeGuardImpl0

幸好,MakeObjGuard让你免于写那些跟字符型图标一样单调的类型。工作机制还是一样——当guard离开作用域,临时对象的析构函数会被调用。析构函数通过指向成员的指针调用成员函数。这里我们用到了.*操作符。

错误处理

如果你读过Herb Sutter关于异常的著作[2],你就会知道一条基本原则:析构函数不应该抛出异常。一个会抛出异常的析构函数会让你无法写出正确的代码,并且会再没有任何警告的情况下终止你的应用程序。在C++里,当一个异常被抛出,在堆栈展开(unwinding)的时候某个析构函数又抛出另一个异常,应用程序会被马上终止。

ScopeGuardImplX和ObjScopeGuardImplX分别调用了一个未知的函数或成员函数,那个函数可能会抛出异常。这会终止程序,因为我们设计guard的析构函数的目的就是:当有异常被抛出,在展开(unwinding)堆栈时,调用这个未知函数!理论上,你不应该把可能抛出异常的函数传给MakeGuard或者MakeObjGuard。在实用中(你可以从供下载的代码中看到),析构函数对异常采取了防御措施。

template< class Obj, typename MemFun>

class ObjScopeGuardImpl0 : public ScopeGuardImplBase

{

...

public:

~ScopeGuardImpl1()

{

if (!dismissed_)

try { (obj_.*fun_)(); }

catch(...) {}

}

}

是的,catch(...)块什么事也不做。这可不是随手写的,这在异常处理的领域中是很基本的:如果你的“撤销/恢复”操作也失败了,那么你几乎没有什么事情可以做了。你尝试恢复,但是不管恢复操作是否成功,你都应该继续下去。

以我们的即时消息为例,一个可能动作顺序是:你向数据库里加入了一个好友数据,但当把它插入friends_ vector时失败了,当然你会尝试把它从数据库里再删掉。虽然可能性很小,但是从数据库里删除数据时,不知道为什么也失败了,这种情况就很讨厌了。

一般来说,你应该在那些保证可以成功撤销的操作上使用guard。

支持传引用的参数

在Petru和我很高兴地使用ScopeGuard一段时间以后,我们遇到一个问题。考虑下面的代码:

void Decrement(int& x) { --x; }

void UseResource(int refCount)

{

++refCount;

ScopeGuard guard = MakeGuard(Decrement, refCount);

...

}

上面代码中的guard对象确保refCount的值在UseResource函数退出时保持不变。(这个惯用法在一些共享资源的情况下很有用。)

尽管有用,但上面的代码不能工作。问题在于,ScopeGuard保存了refCount的一个拷贝(看一下ScopeGuardImpl1的定义,在成员变量parm_里)而不是对它的引用。然而在这个例子里,我们需要的是保存refCount的一个引用,这样才能让Decrement对它进行操作。

一个解决办法是再实现一些类,例如ScopeGuardImplRef,以及MakeGuardRef。这会有很多重复劳动,并且在实现处理多参数的类时,这个办法就很难应付了。

我们采取的办法是:使用一个辅助类,它把引用转变为一个值。

template< class T>

class RefHolder

{

T& ref_;

public:

RefHolder(T& ref) : ref_(ref) {}

operator T& () const

{

return ref_;

}

};

template< class T>

inline RefHolderByRef(T& t)

{

return RefHolder(t);

}

RefHolder以及和它配套的辅助函数ByRef可以无缝地使引用适合于值的语义,并且使ScopeGuardImpl1不需要任何改变就可以使用引用。你要做的只是把引用形式的参数用ByRef包装一下,就象这样:

void Decrement(int& x) { --x; }

void UseResource(int refCount)

{

++refCount;

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

...

}

我们发现这个方法很有说明性,它提醒你正在用引用方式传递参数。

这个支持引用的办法最好的一点是在ScopeGuardImpl1中的const修饰。这里是相关的代码摘要:

template< typename Fun, typename Parm>

class ScopeGuardImpl1 : public ScopeGuardImplBase

{

...

private:

Fun fun_;

const Parm parm_;

};

这个小小的const非常重要。它防止使用非const引用的代码通过编译和不正确地运行。换句话说,如果你忘记使用ByRef,编译器不会让这样的错误代码通过。

再等一下,还有一点

到现在为止,你有了一个好工具可以帮助你写出正确的代码,而不用发愁。然而有时候你会想要ScopeGuard在退出一个代码块时始终执行。这种情况下,定义一个ScopeGuard类型的哑变量很麻烦——你只需要一个临时变量,而不需要给它命名。

宏ON_BLOCK_EXIT可以做到这样,你可以这样写出表达力更好的代码:

{

FILE* topSecret = fopen('cia.txt');

ON_BLOCK_EXIT(std::fclose, topSecret);

... use topSecret ...

} // topSecret automagically closed

ON_BLOCK_EXIT表示:“我希望在当前代码块退出时做这个动作。”类似的,ON_BLOCK_EXIT_OBJ对于成员函数调用实现相同的功能。

这些宏使用了不太正统的(虽然合法的)花招,这里就不公开了。如果你好奇,你可以到代码里去查看这些宏(因为编译器的bug,用Microsoft VC++的朋友必须关闭“Program Database for Edit and Continue”设定,否则ON_BLOCK_EXIT会有问题)。

现实中的ScopeGuard

我们喜欢 ScopeGuard是因为它易于使用和概念简单。这篇文章详细讲了整个实现,但是要解释ScopeGuard的用法只要几分钟。ScopeGuard在我们的同事中间象野火一样迅速传播开来,每个人都认为它是一个很有价值的工具,很多情况下有助于防止因为异常而过早返回。有了ScopeGuard,你可以轻松地编写异常安全的代码,而且理解和维护也同样简单。

每个工具都有推荐的使用方法,ScopeGuard也不例外。你应该象 ScopeGuard期望的那样使用它——作为函数中的自动变量。你不应该把ScopeGuard对象用作成员变量,或者在堆上分配它们。为此,供下载的代码中包含了一个Janitor类,它和ScopeGuard做的事情一样,但是采取了更通用的做法——代价是损失了一些效率。因为编译器的bug, Borland C++ 5.5的用户需要使用Janitor而不是ScopeGuard。

结论

我们讨论了一些在编写异常安全代码中出现的一些情况。在比较了几个在这些情况下获得异常安全性的方法以后,我们介绍了一个方法,适用于有防错性(并且不会再throw)撤销操作可用的情况。ScopeGuard使用了若干泛型编程的技术,让你指定在ScopeGuard退出代码块时调用的函数和成员函数。作为可选项,你也可以解除 ScopeGuard对象的动作。

当你需要实行资源的自动释放,并且可以依靠防错的撤销操作,ScopeGuard在着这种情况下对你很有帮助。当你把几个可能会失败,但是也可以撤销的操作组成一个原子操作,这个惯用法就变得很重要了。当然这个方法也有不适用的情况。

致谢

Herb Sutter对本文进行了特别的技术审查。作者也感谢Mihai Antonescu和Dan Pravat对本文所做的修正以及所提的建议。

参考资料

[1] Bjarne Stroustrup. The C++ Programming Language, 3rd Edition (Addison-Wesley, 1997), page 366.

[2] Herb Sutter. Exceptional C++: 47 Engineering Puzzles, Programming Problems, and Solutions (Addison-Wesley. 2000)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值