c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard

时间:2023-12-27 18:45:37

分类: C/C++

转载:http://blog.csdn.net/fangqu/article/details/4242245
----------------------------------------------------------------------------------------
作者:Andrei Alexandrescu and Petru Marginean

原文地址:http://www.ddj.com/cpp/184403758

翻译,裁剪,修改:purewinter

注:裁剪修改只是为了让更多csdn上的读者不会因为此文太长而放弃阅读。。。

注2:Loki::ScopeGuard不仅对通常意义的异常有用,对于所有可以使用RAII的地方均有用。包括new出来的内存空间的管理,FILE或CFile之类的文件句柄等。

第一次翻译,不足之处多多指教。

可能有异常出现的时候,编写正确的代码并不是一件简单的工作,这是我们都要面对的问题。异常导致了一个与程序主控制流几乎无关的控制流。因此,解决异常流的问题也就需要另外一种途径,以及另外一种工具。

编写异常安全代码很困难—— 一个普通的例子

(译注:原文这个例子介绍得很详细,此处将只说要点)假设一个用户管理中,可以为已登陆的用户添加好友。好友将添加至内存列表以及数据库。下面是部分源代码:

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuardclass User

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard...{

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    ...

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    string GetName();

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    void AddFriend(User& newFriend);

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuardprivate:

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    typedef vector<User*> UserCont;

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    UserCont friends_;

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    UserDatabase* pDB_;

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard};

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuardvoid User::AddFriend(User& newFriend)

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard...{

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    // Add the new friend to the database

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    pDB_->AddFriend(GetName(), newFriend.GetName());  //语句1

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    // Add the new friend to the vector of friends

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    friends_.push_back(&newFriend);  //语句2

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard}

对很多人来说会很吃惊,对于这个两行代码的AddFriend,却有一个致命的bug:如果内存不足导致语句2执行失败,数据库里就会有新好友的记录,而内存中没有——这种不一致是十分危险的。

一个很简单的解决方案就是,把语句1和语句2对调。这样语句2失败时将抛出异常,也就不会执行语句1了。问题是,如果数据库操作抛出异常了呢?这种不一致性就更危险了。嗯,是时候质问那些写数据库代码的人了:“你们这些家伙,就不能返回个错误代码啊?为什么非要抛出异常啊?”那些家伙说了,“嗯,你想,我们是构建在高可靠性的基于TZN网络的XYZ数据库服务器集群上的,所以失败是及其罕见的。既然如此罕见,我们认为最好用异常来表示处理失败,毕竟异常只出现在异常情况下嘛( because exceptions appear only
in exceptional conditions),对吧?”(译注:TZN和XYZ都是随你YY的名称)这似乎很有道理。但是你又不想让一个数据库异常让你的程序跌入混沌深渊...你该怎么办呢? 实际上,你必须要执行两个操作,其中任何一个失败你都要回滚到什么都没做的状态。让我们看看几种实现的方法。

解决方案1:蛮干

很简单,加try-catch就是。

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuardvoid User::AddFriend(User& newFriend)

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard{

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    friends_.push_back(&newFriend);

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    try

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    {

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard        pDB_->AddFriend(GetName(), newFriend.GetName());

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    }

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    catch (...)

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    {

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard        friends_.pop_back();

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard        throw;

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    }

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard}

如果push_back失败了,那没关系,因为这样的话AddFriend不会执行。若AddFriend失败,那么会被catch,然后执行pop_back,最后漂亮地把异常重新抛出。嗯,这的确行得通。但是这样做也问题多多:

代码增加并且显得很笨拙。2行的代码变成了10行,而且这样及其令人反感:想象一下你的代码里到处是这样的try-catch.....



更严重的是,这样的代码很难扩展。想象一个有第三条语句要加进来。那将使代码变得更笨拙,(由于三条语句任意一条都可以失败,)你得写嵌套的try,或者自己写一种复杂的流控制标志和语句。(译注:我是不敢想象,因为我就写过,实在是痛苦。)

这种方法不仅使代码膨胀,效率低下,更严重的是是可读性变得很差,也难以维护。

解决方案2:原则上正确的途径

给任何一个真正熟悉C++的人说说,他们马上会说,“噢,你应该使用RAII(resource acquisition isinitialization)来解决这个问题。在失败的情况下让析构函数自动处理资源释放(或回滚)。”OK,让我们来试试。对于每一个你需要undo的操作,都需要相应的一个class。这个class的构造函数执行那个操作,而析构函数回滚它,除非你调用了一个“提交”函数,告诉析构函数不要回滚操作。对于push_back操作,可能的class代码如下:

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuardclass VectorInserter

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard{

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuardpublic:

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    VectorInserter(std::vector& v, User& u)

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    : container_(v), commit_(false)

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    {

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard        container_.push_back(&u);

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    }

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    void Commit() throw()

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    {

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard        commit_ = true;

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    }

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    ~VectorInserter()

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    {

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard        if (!commit_) container_.pop_back();

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    }

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuardprivate:

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    std::vector& container_;

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    bool commit_;

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard};

这其中最重要的可能就是在Commit函数定义旁的“throw()”描述符了。它指出了Commit操作永远会成功这个事实。而实际上Commit只是告诉VectorInserter:“一切正常,不要回滚任何操作。”你将这样使用这个类:

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuardvoid User::AddFriend(User& newFriend)

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard{

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    VectorInserter ins(friends_, &newFriend);

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    pDB_->AddFriend(GetName(), newFriend.GetName());

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    // Everything went fine, commit the vector insertion

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    ins.Commit();

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard}

AddFriend现在有两个部分:做实际操作的部分,还有提交的部分。提交的部分不会发生异常:它只是阻止回滚而已。这种方法在任何情况下都工作得很好。如果push_back操作失败,由于ins对象构建失败,自然下面的操作以及ins的析构函数都不会执行。(ins都没构建完毕,自然无所谓析构)如果数据库操作失败,那么ins的析构函数将会执行pop_back。

这个方案看上去很不错,实际上工作得也很好,但在现实中却还是有一些麻烦。你得为※每个※需要回滚的操作额外写一个类,于是你的类列表里就多出了相当多的这种类。重复的写这种类显然不是一种好主意,而且很容易出错——没注意到吗?VectorInserter就有一个bug:它的拷贝构造函数干了一件很不好的事情。(天哪!那将会pop_back多次......)定义类本来就不是一件简单的事情,这也是不要写很多这种类的另一个理由。

解决方案3:普遍的现实

无论你看完了刚才所有的讨论或者根本就没看,你知道你最终的方案是什么?当然,你很清楚,就是这样:

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuardvoid User::AddFriend(User& newFriend)

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard{

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    friends_.push_back(&newFriend);

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    pDB_->AddFriend(GetName(), newFriend.GetName());

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard}

这是一种建立在并不科学的论据的基础上的方案:“谁说内存不足了?这里还有狗X的一半多呢!”“就算内存用光了,不是还有内存分页机制会使程序变成蜗牛的速度来避免程序崩溃吗!”“那些搞数据库的家伙说了,那个AddFriend数据库操作几乎不可能失败!他们可是用了XYZ和TZN!”“这根本不值得烦恼。我们会在以后的review中再解决。”于是,最终代码就成了这样。随着时间的流逝以及日程压力,那些本来只在“理论”上的问题开始浮现,而你却不知道它是由于硬件问题还是由于异常。随着用户数量的增加,你的程序逐渐耗尽内存,你的网络管理员可能因为性能抖动太大而关闭分页机制,你的数据库可能变得不那么可靠.....而你对此没有任何准备。

解决方案4:Petru的办法(Loki::ScopeGuard)

用Loki::ScopeGuard,你可以轻松,正确,高效地写出代码:

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuardvoid User::AddFriend(User& newFriend)

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard{

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    friends_.push_back(&newFriend);

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    ScopeGuard guard = MakeObjGuard(friends_, &UserCont::pop_back);

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    pDB_->AddFriend(GetName(), newFriend.GetName());

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    guard.Dismiss();

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard}

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard

guard做的唯一工作就是当它离开它的作用范围时调用friends_.pop_back,除非你调用了Dismiss(如果那样,guard就什么也不做)。ScopeGuard实现了在它的析构函数里自动对函数或成员函数的调用。(译注:看到RAII方法的缺陷时就应该要想到,ScopeGuard其实就是通用的RAII方法的实现。因此,它有RAII的一切优点,并且避免了其缺点。)

你将这样使用ScopeGuard:如果你有一些操作需要“要么全做,要么都不做”(原子性),那就在每一个操作后面放一个ScopeGuard。在全部完成以后,再全部提交。(提交是不会异常的,因此对提交顺序没有要求)

ScopeGuard对普通函数同样有用(注意上面的例子是对类的成员函数):

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuardvoid* buffer = std::malloc(1024);

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuardScopeGuard freeIt = MakeGuard(std::free, buffer);

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuardFILE* topSecret = std::fopen("cia.txt");

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuardScopeGuard closeIt = MakeGuard(std::fclose, topSecret);

(译注:上面的普通函数的例子和前面的异常安全的例子还有不同:上面的例子是解决“释放资源”,而异常安全的例子是为了“自动回滚”。其实,ScopeGuard真正是适用于任何需要RAII情况,反而不是适用于任何需要异常安全情况。另外,由于资源是肯定要释放的,所以基本上不会Dismiss。也就可以用下面的宏:

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard{

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    FILE* topSecret = fopen("cia.txt");

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    ON_BLOCK_EXIT(std::fclose, topSecret);

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    ... use topSecret ...

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard} // topSecret automagically closed

关于ScopeGuard于异常安全的其他讨论,我可能会新开一贴。)

如果所有操作都成功了,你将Dismiss所有操作。否则,每一个成功构建的ScopeGuard将自动调用你初始化时给它的函数。这样你就不必为了pop_back,关闭文件等undo操作写一个专用的类。这使得ScopeGuard成为一个很有用的轻松的写异常安全代码的可重用工具。(译注:如果一个undo操作要执行一系列函数,Loki有MultiMethods。但是若对应不到一个函数.....就要自己手写了。这使我想到Java里的匿名函数...要是C++能支持多好。)

使用注意

(译注:为了照顾大多数国人的习惯,我把这个从文章最后移了上来并做了修改,否则估计很多人会无视它。)

你应该像它本意一样使用它:当做函数里的变量。你不应该把它用做类的成员变量,不应该把它放到vector里去,不应该在堆上创建它。(如果你要这么做,应该使用Janitor类,但这会导致一些性能损失。)



另外,像在下文里提到的,虽然ScopeGuard中有防范措施,但最好不要传入一个可能抛出异常的回滚操作。毕竟,回滚的时候失败了,那该怎么办?(所以说ScopeGuard并不适用于任何异常安全情况。)

实现ScopeGuard

(译注:以下内容涉及C++ 模版知识,还不清楚的请复习一下:C++ Primer,C++ Templates,Effective STL随你挑一本看看吧。)

ScopeGuard是RAII的一种泛化实现。它与RAII的不同是ScopeGuard只关注清理的部分——资源申请你来做,ScopeGuard帮你清除。清理(回收)资源有很多办法,如调用一个函数/仿函数,调用一个对象的成员函数。它们都可能需要0个,1个或多个参数。自然的,我们建立了一个类的继承体系来处理这种差异。在这个继承体系的对象的析构函数来做实际的工作。而这个继承体系的基类,ScopeGuardImplBase,定义如下:

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuardclass ScopeGuardImplBase

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard{

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuardpublic:

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    void Dismiss() const throw()

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    {    dismissed_ = true;    }

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuardprotected:

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    ScopeGuardImplBase() : dismissed_(false)

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    {}

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    ScopeGuardImplBase(const ScopeGuardImplBase& other)

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    : dismissed_(other.dismissed_)

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    {    other.Dismiss();    }

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    ~ScopeGuardImplBase() {} // nonvirtual (see below why)

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    mutable bool dismissed_;

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuardprivate:

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    // Disable assignment

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    ScopeGuardImplBase& operator=(

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard        const ScopeGuardImplBase&);

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard};

ScopeGuardImplBase管理dismissed_标志,而它决定子类是否执行清除操作。注意到,我们的析构函数并没有使用"virtual"关键字。控制住你的好奇心,我们有一个好方法来获得多态行为却不需要virtual关键字。(译注:噢,他说的是多态行为,而不是析构函数,虽然这个“多态”的确是针对析构函数的。这里用protected来保证这东西不会被用户代码胡乱的析构。当然,如果你非要继承它然后自己胡乱析构。。。那应该由你自己负责。)至于现在,让我们看看我们怎样实现一个对象:它会在析构时调用一个只有一个参数的函数或仿函数,并且如果调用Dismiss方法,它在析构时不做任何事。

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuardtemplate <typename Fun, typename Parm>

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuardclass ScopeGuardImpl1 : public ScopeGuardImplBase

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard{

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuardpublic:

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    ScopeGuardImpl1(const Fun& fun, const Parm& parm)

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    : fun_(fun), parm_(parm) 

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    {}

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    ~ScopeGuardImpl1()

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    {

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard        if (!dismissed_) fun_(parm_);

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    }

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuardprivate:

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    Fun fun_;

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    const Parm parm_;

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard};

为了能方便地使用ScopeGuardImpl1,我们定义一个辅助函数:

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuardtemplate <typename Fun, typename Parm>

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuardScopeGuardImpl1<Fun, Parm>

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuardMakeGuard(const Fun& fun, const Parm& parm)

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard{

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    return ScopeGuardImpl1<Fun, Parm>(fun, parm);

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard}

就像STL里make_pair和bind1st等等一样,这种辅助函数使你不必输入模版参数。实际上,你不必显式地创建一个ScopeGuardImpl1对象。还在奇怪我们如何实现多态行为却不需要virtual的吗?OK,让我们看看ScopeGuard的定义吧:

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuardtypedef const ScopeGuardImplBase& ScopeGuard;

令人惊奇的是,它只是个typedef。现在让我们解开这个谜团。根据C++标准,一个由临时变量定义的引用,将导致改临时变量的生命周期延长至和此引用一样长。还是用一个例子来说明吧。如果你写下下面代码:

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuardFILE* topSecret = std::fopen("cia.txt");

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuardScopeGuard closeIt = MakeGuard(std::fclose, topSecret);

那么MakeGuard将会生成如下类型的一个临时变量:

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuardScopeGuardImpl1<int (&)(FILE*), FILE*>

然后这个临时变量将会被赋值给你定义的引用类型的常量closeit。本来这个临时变量的生命周期到此结束。但是由于上述标准的规定,这个临时变量的生命周期被延长了。于是,在closeit退出作用范围时,临时变量进行析构——自然这时调用的是正确的析构函数。(译注:这是因为并非由closeit来调用析构函数,只是closeit生命结束时临时变量同时进行析构而已。的确是巧妙的多态。但是,若基类的析构函数不是protected,则用户代码将可能进行错误析构。)在这里,析构函数将关闭打开的文件。

ScopeGuardImpl1支持有一个参数的函数/仿函数。很容易写出类似的支持有0个,2个,3个等等参数的函数的实现(ScopeGuardImpl0,ScopeGuardImpl2,ScopeGuardImpl3...)。如果你完成了这些,那么可以简单的实现MakeGuard的重载,比如:

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuardtemplate <typename Fun>

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuardScopeGuardImpl0<Fun>

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuardMakeGuard(const Fun& fun)

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard{

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    return ScopeGuardImpl0<Fun >(fun);

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard}

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard...

到现在我们已经完成了所有对类C API界面的函数支持,并且没有使用虚函数。让我们再接再厉...

让ScopeGuard 支持类成员函数

下面该加入对类成员函数的支持了。(译注:研究过funtor的人应该知道,不过是对付operator.*和operator->*而已,更何况这里不需要更复杂情况的支持。)其实一点也不困难,让我们实现ObjScopeGuardImpl0,让它支持需要0个参数的类成员函数。

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuardtemplate <class Obj, typename MemFun>

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuardclass ObjScopeGuardImpl0 : public ScopeGuardImplBase

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard{

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuardpublic:

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    ObjScopeGuardImpl0(Obj& obj, MemFun memFun)

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    : obj_(obj), memFun_(memFun) 

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    {}

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    ~ObjScopeGuardImpl0()

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    {

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard        if (!dismissed_) (obj_.*fun_)();

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    }

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuardprivate:

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    Obj& obj_;

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    MemFun memFun_;

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard};

除了使用成员函数指针那里的古怪语法,其他没什么特别。让我们看看该怎么实现MakeObjGuard:

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuardtemplate <class Obj, typename MemFun>

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuardObjScopeGuardImpl0<Obj, MemFun, Parm>

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuardMakeObjGuard(Obj& obj, Fun fun)

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard{

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    return ObjScopeGuardImpl0<Obj, MemFun>(obj, fun);

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard}

还是没什么区别。再看看怎么使用:

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuardScopeGuard guard = MakeObjGuard(friends_, &UserCont::pop_back);

注意那个"&"。这次我们传进去的是成员函数的指针,所以需要一个取址操作符。而MakeObjGuard将返回一个具有如下类型的临时变量:

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuardObjScopeGuardImpl0<UserCont, void (UserCont::*)()>

幸运的是,如同MakeGuard,并不需要我们来输入一个如此奇怪的类型。就这样,对成员函数的支持也完成了,与对普通函数的支持区别不大。

错误处理

你应该清楚,类的析构函数是绝对不能(must not)抛出异常的。如果一个类的析构函数抛出异常,将导致未定义的行为。(译注:也就是什么事都可能发生,包括你的程序啪地就没了。至于你的电脑会不会啪地......那得看编译器、操作系统和硬件了。......喂,我说的是啪地就黑了,不要想歪了。)由于ObjScopeGuardImplX和ScopeGuardImplX的析构函数调用的都是用户提供的函数,而它们是有可能抛出异常的。在理论上,你不应该把可能抛出异常的函数传给MakeGuard或MakeObjGuard,在实际上,析构函数有如下的保护:

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuardtemplate <class Obj, typename MemFun>

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuardclass ObjScopeGuardImpl0 : public ScopeGuardImplBase

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard{

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    ...

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuardpublic:

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    ~ScopeGuardImpl1()

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    {

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard        if (!dismissed_)

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard            try { (obj_.*fun_)(); }

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard            catch(...) {}

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    }

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard}

(译注:而在现在的版本,这个try-catch是放到Base的SafeExecute函数中去了。如下:)

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard//在Base中加入此成员模版函数:

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuardtemplate <typename J>

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard        static void SafeExecute(J& j) throw() 

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard        {

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard            if (!j.dismissed_)

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard                try

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard                {

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard                    j.Execute();

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard                }

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard                catch(...)

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard                {}

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard        }

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard//在子类中使用如下代码:

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard~ObjScopeGuardImpl0() throw() 

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard        {

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard            SafeExecute(*this);

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard        }

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard        void Execute() 

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard        {

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard            fun_();

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard        }

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard

不过,不管怎样,使用不会抛出异常的函数才是最好的。

支持引用传参

到现在我们都很开心地使用ScopeGuard,直到我们被下面这个问题困住了:

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuardvoid Decrement(int& x) { --x; }

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuardvoid UseResource(int refCount)

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard{

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    ++refCount;

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    ScopeGuard guard = MakeGuard(Decrement, refCount);

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    ...

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard}

上面的guard对象保证了refCount的值将被保存直到退出UseResource。不管这个想法是否有用,上面的代码并不起作用。问题是,ScopeGuard保存了refCount的一个拷贝。(见ScopeGuardImpl1的定义)而现在我们需要保持refCount的一个引用,这样Decrement才能操作它。一种解决方案是实现额外的类,不过这将导致大量重复。并且如果你遇到多个变量,有的要引用有的不需要,那将十分麻烦。我们依然使用小巧的辅助类来解决这个问题:

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuardtemplate <class T>

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuardclass RefHolder

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard{

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    T& ref_;

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuardpublic:

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    RefHolder(T& ref) : ref_(ref) {}

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    operator T& () const

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    {

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard        return ref_;

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    }

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard};

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuardtemplate <class T>

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuardinline RefHolder<T> ByRef(T& t)

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard{

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    return RefHolder<T>(t);

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard}

RefHolder和它的辅助函数ByRef是很精巧的。它们无缝的将引用适配成一个值,并允许ScopeGuardImpl1和引用一起工作而不需要任何改动。(译注:并且在最后用户函数调用时又会把引用传给它。)而你只需要把你的引用交给ByRef来包装,如下:

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuardvoid Decrement(int& x) { --x; }

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuardvoid UseResource(int refCount)

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard{

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    ++refCount;

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    ScopeGuard guard = MakeGuard(Decrement, ByRef(refCount));

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    ...

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard}

我们发现这个解决方案是十分有表现力和创见的。最棒的是那个在ScopeGuardImpl1中用到的"const"修饰符。相关部分摘录如下:

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuardtemplate <typename Fun, typename Parm>

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuardclass ScopeGuardImpl1 : public ScopeGuardImplBase

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard{

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    ...

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuardprivate:

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    Fun fun_;

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    const Parm parm_;

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard};

这个小小的const是十分重要的。它防止了使用non-const引用而导致的编译和运行错误。换句话说,如果你忘记使用ByRef,编译器会向你抱怨而拒绝工作。(译注:当然,这要求你的函数接受的就是一个引用参数。如果你提供的函数不是接受引用参数,那你就可以传non-const变量或non-const引用给它。这才是它的精巧之处。)

等等,还有更多。。。

现在你拥有了编写异常安全代码的所有需要的东西,不必再为此烦恼了。不过,有时候你需要ScopeGuard永远在你退出(定义它)的语句块时都执行。在这种情况下,再声明一个ScopeGuard变量显得有些累赘——你只需要一个临时变量,一个没有名字的临时变量。这个时候你就可以使用ON_BLOCK_EXIT宏。

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard{

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    FILE* topSecret = fopen("cia.txt");

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    ON_BLOCK_EXIT(std::fclose, topSecret);

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    ... use topSecret ...

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard} // topSecret automagically closed

ON_BLOCK_EXIT声称:“我希望在当前块退出时执行这个操作。”类似的,ON_BLOCK_EXIT_OBJ为类成员函数实现了类似的功能。

这两个宏使用了非传统(虽然合法)的宏技巧,这种技巧将不公开(译注:也不特别,没必要不公开吧。)对它们感到好奇的人可以到源代码中找到它们。

在真实世界中的ScopeGuard

可能ScopeGuard最棒的地方就是它的容易使用和概念简单了。这篇文章详细地介绍了整个实现,不过介绍ScopeGuard的使用方法只用了一小部分时间。在我的同仁中,ScopeGuard像野火般传播开去。每个人都把ScopeGuard当作一个对很多情况都很有用的工具,无论是无序返回或者异常。在ScopeGuard的帮助下,你最终可以轻松地编写异常安全代码,并且也能轻松地理解和维护它。

后记:

上述的代码在VC6中可以运行(但不能同RefHolder一起),但是不能在VC8中运行。原因是MakeGuard函数自动推导模版参数失败:不能从重载函数到重载函数进行参数推导。而目前Loki的代码已改正这个问题,就是在每个ImplX里加入static的MakeGuard模版函数,而全局MakeGuard模版函数制定使用的类名。相关修改如下:

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard template <typename F, typename P1>

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    class ScopeGuardImpl1 : public ScopeGuardImplBase

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    {

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    public:

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard        static ScopeGuardImpl1<F, P1> MakeGuard(F fun, P1 p1)

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard        {

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard            return ScopeGuardImpl1<F, P1>(fun, p1);

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard        }

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard   ....

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard  };

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard//对应的全局MakeGuard模版函数:

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuardtemplate <typename F, typename P1> 

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    inline ScopeGuardImpl1<F, P1> MakeGuard(F fun, P1 p1)

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    {

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard        return ScopeGuardImpl1<F, P1>::MakeGuard(fun, p1);

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard    }

c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard

具体原因尚不清楚。似乎是由于构造函数有重载,所以导致推导失败,而指定调用类中的一个函数以后,由于后者没有重载(只有模版参数),所以推导成功。