你(真的)写出异常安全的代码吗?

异常处理(EH)似乎是当前的标准,并且通过搜索网络,我找不到任何尝试改进或取代它的新颖想法或方法(当然,存在一些变化,但没有新颖性)。

尽管大多数人似乎忽略了它,或者只是接受它,但EH 一些巨大的缺点:代码中看不到异常,它创造了许多可能的退出点。 乔尔在软件上写了一篇关于它的文章。 与goto的比较非常适合,这让我再次想到了EH。

我尽量避免使用EH,只使用返回值,回调或任何符合目的的东西。 但是当你必须编写可靠的代码时,你现在不能忽略EH :它以new开始,它可能会抛出一个异常,而不是仅仅返回0(就像过去一样)。 这使得任何一行C ++代码都容易受到异常的影响。 然后,C ++基础代码中的更多地方会抛出异常...... std lib会执行此操作,依此类推。

这感觉就像走在摇摇欲坠的理由上 。所以,现在我们被迫关心异常!

但它很难,它真的很难。 你必须学会​​写异常安全的代码,即使你有一些经验,仍然需要仔细检查任何一行代码才能安全! 或者你开始在任何地方放置try / catch块,这会混淆代码,直到达到不可读状态。

EH取代了旧的清晰的确定性方法(返回值..),它只有一些但易于理解和易于解决的缺点,并在代码中创建了许多可能的退出点,并且如果您开始编写捕获异常的代码(您在某些时候被迫做),然后它甚至通过你的代码创建了许多路径(在catch块中的代码,想想你需要日志工具而不是std :: cerr的服务器程序..)。 EH有优势,但这不是重点。

我的实际问题:

  • 你真的写出异常安全的代码吗?
  • 你确定你最后的“生产就绪”代码是异常安全的吗?
  • 你甚至可以肯定,这是吗?
  • 你知道和/或实际使用替代品吗?

  • 你的问题提出了一个断言,即“编写异常安全代码非常困难”。 我会先回答你的问题,然后回答隐藏在他们身后的问题。

    回答问题

    你真的写出异常安全的代码吗?

    我当然是了。

    这就是Java作为一名C ++程序员(缺乏RAII语义)失去了很多吸引我的原因,但我离题了:这是一个C ++问题。

    当您需要使用STL或Boost代码时,它实际上是必需的。 例如,C ++线程( boost::threadstd::thread )将抛出一个异常以优雅地退出。

    你确定你最后的“生产就绪”代码是异常安全的吗?

    你甚至可以肯定,这是吗?

    编写异常安全的代码就像编写无错代码一样。

    你不能100%确定你的代码是异常安全的。 但是,那么你就会努力使用众所周知的模式,并避免众所周知的反模式。

    你知道和/或实际使用替代品吗?

    在C ++中没有可行的替代方案(即,您需要恢复到C,并避免使用C ++库,以及Windows SEH等外部意外)。

    编写异常安全代码

    编写异常安全的代码,您必须首先了解每个你写的指令是什么级别的异常安全的。

    例如,一个new可以抛出异常,但分配一个内置的(例如一个int或一个指针)不会失败。 一个交换将永远不会失败(不要写一个抛出交换),一个std::list::push_back可以抛出...

    例外保证

    首先要了解的是,您必须能够评估所有功能提供的异常保证:

  • none :你的代码不应该提供这个。 这段代码会泄漏一切,并在抛出的第一个异常处发生故障。
  • 基本 :这是您至少必须提供的保证,也就是说,如果抛出异常,不会泄漏资源,并且所有对象仍然是整体
  • :处理过程要么成功,要么抛出异常,但是如果抛出,那么数据将处于相同的状态,就好像处理还没有开始一样(这给C ++一个事务性的权力)
  • nothrow / nofail :处理将成功。
  • 代码示例

    下面的代码看起来像是正确的C ++,但事实上,它提供了“无”保证,因此它是不正确的:

    void doSomething(T & t)
    {
       if(std::numeric_limits<int>::max() > t.integer)  // 1.   nothrow/nofail
          t.integer += 1 ;                              // 1'.  nothrow/nofail
       X * x = new X() ;                // 2. basic : can throw with new and X constructor
       t.list.push_back(x) ;            // 3. strong : can throw
       x->doSomethingThatCanThrow() ;   // 4. basic : can throw
    }
    

    我在脑海中编写了我所有的代码,并进行了这种分析。

    提供的最低保证是基本的,但是,每条指令的顺序使得整个函数“无”,因为如果3.抛出,x将泄漏。

    首先要做的是使函数“基本”,即把x放在智能指针中,直到它被列表安全拥有:

    void doSomething(T & t)
    {
       if(std::numeric_limits<int>::max() > t.integer)  // 1.   nothrow/nofail
          t.integer += 1 ;                              // 1'.  nothrow/nofail
       std::auto_ptr<X> x(new X()) ;    // 2.  basic : can throw with new and X constructor
       X * px = x.get() ;               // 2'. nothrow/nofail
       t.list.push_back(px) ;           // 3.  strong : can throw
       x.release() ;                    // 3'. nothrow/nofail
       px->doSomethingThatCanThrow() ;  // 4.  basic : can throw
    }
    

    现在,我们的代码提供了“基本”保证。 什么都不会泄漏,并且所有的物体都会处于正确的状态。 但我们可以提供更多,也就是有力的保证。 这是成本高昂的地方,这也是为什么并非所有 C ++代码都很强大的原因。 让我们试试看:

    void doSomething(T & t)
    {
       // we create "x"
       std::auto_ptr<X> x(new X()) ;    // 1. basic : can throw with new and X constructor
       X * px = x.get() ;               // 2. nothrow/nofail
       px->doSomethingThatCanThrow() ;  // 3. basic : can throw
    
       // we copy the original container to avoid changing it
       T t2(t) ;                        // 4. strong : can throw with T copy-constructor
    
       // we put "x" in the copied container
       t2.list.push_back(px) ;          // 5. strong : can throw
       x.release() ;                    // 6. nothrow/nofail
       if(std::numeric_limits<int>::max() > t2.integer)  // 7.   nothrow/nofail
          t2.integer += 1 ;                              // 7'.  nothrow/nofail
    
       // we swap both containers
       t.swap(t2) ;                     // 8. nothrow/nofail
    }
    

    我们重新排序了操作,首先创建并将X设置为正确的值。 如果任何操作失败,那么t不会被修改,所以操作1到3可以被认为是“强”:如果抛出异常, t不被修改,并且X不会因为智能指针拥有而泄漏。

    然后,我们创建一个副本t2t ,并从操作4这个副本工作7.如果抛出的东西, t2被修改,但随后, t仍然是原来的。 我们仍然提供强有力的保证。

    然后,我们交换tt2 。 Swap操作应该不会在C ++中出现,所以我们希望你为T写的交换不是空的(如果不是,重写它,这样它就不会出现)。

    所以,如果我们到达函数的末尾,一切都成功了(不需要返回类型)并且t有它的例外值。 如果失败了,那么t仍然是它的原始价值。

    现在,提供强有力的保证可能会非常昂贵,所以不要努力为您的所有代码提供强有力的保证,但是如果您可以在没有成本的情况下做到这一点(并且C ++内联和其他优化可以使所有代码成本高于成本) ,然后做。 功能用户会感谢你。

    结论

    编写异常安全的代码需要一些习惯。 您需要评估您将使用的每条说明所提供的保证,然后,您需要评估一系列说明所提供的保证。

    当然,C ++编译器不会提供保证(在我的代码中,我提供了一个@warning doxygen标签),这有点令人伤心,但它不应该阻止您尝试编写异常安全的代码。

    正常失败与错误

    程序员如何保证nofail函数总能成功? 毕竟,该功能可能有一个错误。

    这是真的。 异常保证应该由无错代码提供。 但是,在任何语言中,调用一个函数都假设该函数没有bug。 没有理智的代码可以保护自己免受bug的可能性。 编写最好的代码,然后,提供保证,假设它没有bug。 如果有错误,纠正它。

    例外情况是出现异常处理失败,而不是代码错误。

    最后的话

    现在,问题是“这值得吗?”。

    当然如此。 知道函数不会失效的“nothrow / nofail”函数是一个很大的好处。 对于“强”功能也是如此,它使您能够使用事务性语义编写代码,如数据库,具有提交/回滚功能,提交正常执行代码,引发异常是回滚。

    那么,“基本”是你应该提供的最低保证。 C ++是一种非常强大的语言,它的范围可以避免任何资源泄漏(垃圾收集器会发现数据库,连接或文件句柄难以提供)。

    所以,据我看来,这值得的。

    编辑2010-01-29:关于非投掷交换

    nobar发表了一篇评论,我认为这很有意义,因为它是“你如何编写异常安全代码”的一部分:

  • [我]交换将永远不会失败(甚至不写一个投掷交换)
  • [nobar]这是自定义编写的swap()函数的一个很好的建议。 但是,应该注意的是, std::swap()可能会因内部使用的操作而失败
  • 默认的std::swap会进行复制和分配,这对于某些对象可能会抛出。 因此,默认的交换可以抛出,用于你的类,甚至用于STL类。 就C ++标准而言, vectordequelist的交换操作不会抛出,而如果比较函子可以抛出复制构造,它可以用于map (参见C ++编程语言特别版附录E ,E.4.3.Swap)。

    看看向量交换的Visual C ++ 2008实现,如果两个向量具有相同的分配器(即正常情况),则向量的交换不会抛出,但如果它们具有不同的分配器,则会进行复制。 因此,我认为它可以抛出最后一种情况。

    因此,原始文本仍然存在:不要写一个丢弃交换,但必须记住nobar的注释:确保您交换的对象具有非抛出交换。

    编辑2011-11-06:有趣的文章

    Dave Abrahams给了我们一些基本的/强大的/不值得保证的信息,他在一篇文章中描述了他关于STL异常安全的经验:

    http://www.boost.org/community/exception_safety.html

    看看第7点(自动化异常安全测试),他依靠自动化单元测试来确保每个案例都经过测试。 我想这个部分对问题作者的“你甚至可以肯定,它是?”是一个很好的答案。

    编辑2013-05-31:来自dionadar的评论

    t.integer += 1; 没有保证溢出不会发生异常安全,事实上可以在技术上调用UB! (签名溢出是UB:C ++ 11 5/4“如果在评估表达式时,结果不是数学定义的或者不在其类型的可表示值范围内,则行为是未定义的。”)注意,无符号整数不会溢出,但在等价类模2 ^#位中进行计算。

    Dionadar提到了下面这一行,它确实有未定义的行为。

       t.integer += 1 ;                 // 1. nothrow/nofail
    

    这里的解决方案是在添加之前验证整数是否已经达到其最大值(使用std::numeric_limits<T>::max() )。

    我的错误会出现在“正常失败与错误”部分,也就是一个错误。 它不会使推理失效,并不意味着异常安全的代码是无用的,因为无法实现。 您无法保护自己免受计算机关闭,编译器错误,甚至是错误或其他错误的影响。 你不能达到完美,但你可以尝试尽可能接近。

    我根据Dionadar的评论修正了代码。


    用C ++编写异常安全的代码并不是要使用大量try {} catch {}块。 这是关于记录你的代码提供什么样的保证。

    我建议阅读香草萨特的本周系列大师,特别是分期付款59,60和61。

    总而言之,您可以提供三种异常安全级别:

  • 基本:当你的代码抛出一个异常时,你的代码不会泄漏资源,并且对象仍然可以被破坏。
  • 强:当您的代码抛出异常时,它将保持应用程序的状态不变。
  • 没有抛出:你的代码从不抛出异常。
  • 就我个人而言,我很晚才发现这些文章,所以我的C ++代码绝对不是异常安全的。


    我们有些人20多年来一直在使用例外。 例如PL / I有他们。 我认为他们是一种新的危险技术的前提是值得怀疑的。

    链接地址: http://www.djcxy.com/p/13261.html

    上一篇: Do you (really) write exception safe code?

    下一篇: Center a flexbox item in the container, if there is already an item to the left