关于c ++:你(真的)编写异常安全代码吗?

Do you (really) write exception safe code?

异常处理(eh)似乎是当前的标准,通过搜索网页,我找不到任何新的想法或方法来改进或替换它(好吧,有些变化存在,但没有新颖之处)。

虽然大多数人似乎忽视它或只是接受它,但是eh有一些巨大的缺点:异常对代码是不可见的,它创建了许多可能的出口点。关于软件的乔尔写了一篇文章。与goto的比较是完美的,这让我重新思考了eh。

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

这感觉就像在摇摇晃晃的地上行走。所以,现在我们不得不关注异常!

但很难,很难。您必须学会编写异常安全代码,即使您有一些使用它的经验,仍然需要重新检查任何一行代码以确保安全!或者您开始在任何地方放置try/catch块,这会使代码变得混乱,直到达到不可读的状态。

eh用一种在代码中创建许多可能的出口点的方法替换了旧的干净的确定性方法(返回值..),这种方法只有几个可理解的缺点,并且易于解决,如果您开始编写捕获异常的代码(在某个时刻您必须执行的操作),那么它甚至创建了许多路径。通过您的代码(catch块中的代码,考虑一个服务器程序,在该程序中您需要std::cerr….以外的日志记录工具)。嗯,有优势,但这不是重点。

我的实际问题:

  • 你真的写了异常安全代码吗?
  • 您确定最后一个"生产就绪"代码是异常安全的吗?
  • 你能确定吗?
  • 你知道和/或实际使用其他可行的方法吗?


你的问题表明,"编写异常安全代码非常困难"。我会先回答你的问题,然后再回答他们背后隐藏的问题。好的。回答问题

Do you really write exception safe code?

Ok.

当然,我知道。好的。

这就是为什么Java失去了它作为C++程序员的吸引力(缺少RAII语义),但我在解题:这是一个C++问题。好的。

实际上,当您需要使用STL或Boost代码时,这是必需的。例如,C++线程(EDCOX1 0)或EDCOX1(1))将抛出异常以优雅退出。好的。

Are you sure your last"production ready" code is exception safe?

Ok.

Can you even be sure, that it is?

Ok.

编写异常安全代码就像编写没有bug的代码。好的。

你不能百分之百地确定你的代码是异常安全的。但之后,您将努力实现它,使用众所周知的模式,并避免使用众所周知的反模式。好的。

Do you know and/or actually use alternatives that work?

Ok.

在C++中没有可行的替代方案(也就是说,你需要恢复到C,避免C++库,以及像Windows SEH这样的外部惊喜)。好的。编写异常安全代码

要编写异常安全代码,必须首先知道编写的每个指令的异常安全级别。好的。

例如,new可以抛出异常,但分配内置(例如in t或指针)不会失败。一个交换永远不会失败(不要写一个抛出的交换),一个std::list::push_back可以抛出…好的。例外担保

首先要了解的是,您必须能够评估所有职能部门提供的例外担保:好的。

  • 没有:你的代码不应该提供这个。这段代码将泄漏所有信息,并在抛出第一个异常时崩溃。
  • 基本原则:这是您必须提供的保证,即,如果抛出异常,则不会泄漏任何资源,并且所有对象仍然是完整的。
  • St强大:处理将成功,或引发异常,但如果抛出,则数据将处于与处理根本没有启动的相同状态(这给C++提供了事务性电源)。
  • 否/否失败:处理将成功。
  • 代码示例

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

    1
    2
    3
    4
    5
    6
    7
    8
    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放入智能指针中,直到它被列表安全拥有:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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++代码都很强大的原因。让我们试试看:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    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也不会泄漏,因为它属于智能指针。好的。

    然后,我们创建一个tt2副本,并从操作4到7对该副本进行处理。如果有东西抛出,则修改t2,但随后,t仍然是原始的。我们仍然提供强有力的保证。好的。

    然后,我们交换tt2。交换操作应该在C++中不被抛出,所以让我们希望您为EDCOX1的1位写的交换是NoFoT(如果不是,重写它,这样它就不会被抛出)。好的。

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

    现在,提供强有力的保证可能是相当昂贵的,所以不要努力为所有代码提供强有力的保证,但是如果您可以不花费成本(并且C++内联和其他优化可以使所有代码都没有成本),那么就这样做。用户将为此感谢您。好的。结论

    编写异常安全代码需要一些习惯。您需要评估您将要使用的每个指令提供的担保,然后,您需要评估一个指令列表提供的担保。好的。

    当然,C++编译器不会支持这个保证(在我的代码中,我提供的保证是一个警告的doxGEN标签),这有点令人难过,但是它不应该阻止你尝试编写异常安全代码。好的。正常故障与故障

    程序员如何保证一个不失败的函数总是成功的?毕竟,函数可能有一个bug。好的。

    这是真的。异常保证应该由无缺陷代码提供。但是,在任何语言中,调用一个函数都假定该函数是无缺陷的。没有健全的代码可以保护自己不受出现错误的可能性的影响。尽你所能地编写代码,然后假设它是无缺陷的,提供保证。如果有错误,纠正它。好的。

    异常是针对异常处理失败,而不是针对代码错误。好的。最后的话

    现在,问题是"这值得吗?".好的。

    当然是这样。拥有一个"无故障/无故障"功能,知道该功能不会失败是一个很大的好处。对于"强"函数也可以这样说,它使您能够使用事务语义编写代码,例如数据库,具有提交/回滚功能,提交是代码的正常执行,抛出异常是回滚。好的。

    那么,"基本"是你应该提供的最起码的保证。C++是一种非常强大的语言,具有范围,使您能够避免任何资源泄漏(垃圾收集器会发现为数据库、连接或文件句柄提供困难)。好的。

    所以,就我看来,这是值得的。好的。编辑2010-01-29:关于非抛出交换

    Nobar做了一个我认为非常相关的评论,因为它是"如何编写异常安全代码"的一部分:好的。

    • [我]交换永远不会失败(甚至不要写一个抛出的交换)
    • [nobar]这是自定义编写的swap()函数的一个很好的建议。然而,应该注意的是,根据其内部使用的操作,std::swap()可能会失败。

    默认的std::swap将生成副本和分配,对于某些对象,这些副本和分配可以抛出。因此,默认交换可以抛出,可以用于类,甚至用于stl类。就C++标准而言,如果EDCOX1、3、EDCOX1、4、EDCX1、5等的交换操作不会抛出,而如果EXOCX1的6个字可以与EXOCX1相关,则如果比较函数可以抛出拷贝构造(参见C++编程语言,特别版,附录E,E.4.3.SWAP)。好的。

    在Visual C++ 2008实现向量交换时,如果两个向量具有相同的分配器(即正常情况),则向量的交换不会抛出,但如果它们具有不同的分配器,则将复制。因此,我假设它可以在最后一个案例中抛出。好的。

    所以,原始文本仍然保持不变:永远不要编写抛出交换,但必须记住Nobar的评论:确保要交换的对象具有非抛出交换。好的。编辑2011-11-06:有趣的文章

    Dave Abrahams为我们提供了基本的/strong/nothrow保证,他在一篇文章中描述了他在确保STL异常安全方面的经验:好的。

    http://www.boost.org/community/exception_safety.html好的。

    看看第7点(异常安全的自动测试),他依靠自动单元测试来确保每个案例都经过测试。我想这部分是对作者"你能确定吗?".好的。编辑2013-05-31:迪奥纳达尔的评论

    t.integer += 1; is without the guarantee that overflow will not happen NOT exception safe, and in fact may technically invoke UB! (Signed overflow is UB: C++11 5/4"If during the evaluation of an expression, the result is not mathematically defined or not in the range of representable values for its type, the behavior is undefined.") Note that unsigned integer do not overflow, but do their computations in an equivalence class modulo 2^#bits.

    Ok.

    迪奥纳达尔指的是下面这一行,它的行为确实不明确。好的。

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

    这里的解决方案是在执行加法之前验证整数是否已经达到最大值(使用std::numeric_limits::max())。好的。

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

    我把迪奥纳达尔的评论牢记在心,修正了这段代码。好的。好啊。


    在C++中编写异常安全代码与使用大量的尝试{ } catch {}块无关。它是关于记录您的代码提供了什么样的保证。

    我建议阅读Herb Sutter的"本周大师"系列,特别是59、60和61期。

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

    • 基本:当代码抛出异常时,代码不会泄漏资源,对象仍然是可销毁的。
    • 强:当代码抛出异常时,应用程序的状态保持不变。
    • 不抛出:您的代码从不抛出异常。

    就我个人而言,我发现这些文章已经很晚了,所以我的C++代码绝对不例外。


    我们中的一些人使用这个例外已经超过20年了。例如,pl/i有它们。他们是一种新的危险技术的前提在我看来是可疑的。


    首先(如尼尔所说),SEH是微软的结构化异常处理。它与C++中的异常处理类似但不完全相同。事实上,如果要在VisualStudio中使用C++,则必须启用C++异常处理——默认行为不能保证本地对象在所有情况下都被破坏!在这两种情况下,异常处理并不是很困难,只是不同而已。

    现在请回答您的实际问题。

    Do you really write exception safe code?

    对。我在所有情况下都努力寻找异常安全的代码。我使用RAII技术来传播对资源的作用域访问(例如,用于内存的boost::shared_ptr,用于锁定的boost::lock_guard)。通常,RAII和范围保护技术的一致使用将使异常安全代码更容易编写。诀窍是了解存在的东西以及如何应用它。

    Are you sure your last"production ready" code is exception safe?

    不,它和它一样安全。我可以说,在几年的24/7活动中,我没有看到由于异常而导致的流程故障。我不期望完美的代码,只是编写良好的代码。除了提供异常安全性外,上述技术还保证了正确性,这种方法几乎不可能用try/catch块实现。如果您捕获了顶部控制范围(线程、进程等)中的所有内容,那么您可以确保在遇到异常时(大多数情况下)继续运行。同样的技术也将帮助您在遇到异常时继续正确地运行,而不需要在任何地方使用try/catch块。

    Can you even be sure that it is?

    对。你可以通过一个彻底的代码审计来确定,但是没有人真的这样做?不过,定期的代码审查和谨慎的开发人员要想达到这一目标还有很长的路要走。

    Do you know and/or actually use alternatives that work?

    这些年来,我尝试过一些变化,比如高位编码状态(ala HRESULTs)或可怕的setjmp() ... longjmp()黑客。这两种情况在实践中都有不同的分解方式。

    最后,如果您养成了应用一些技术的习惯,并仔细考虑在哪里可以对异常做出响应,那么您将得到非常可读的代码,这是异常安全的。您可以按照以下规则进行总结:

    • 你只想看到try/catch,当你可以对一个特定的异常做些什么的时候。
    • 您几乎不想在代码中看到原始的newdelete
    • 避开std::sprintfsnprintf和数组,一般使用std::ostringstream来格式化数组,并用std::vectorstd::string替换数组。
    • 如果有疑问,请在滚动之前在boost或stl中查找功能

    我只能建议您学习如何正确使用异常,如果您计划用C++编写,则忘记结果代码。如果你想避免例外,你可以考虑用另一种语言来写,要么没有例外,要么使例外安全。如果你想真正学会如何充分利用C++,请阅读萨特、Nicolai Josuttis和Scott Meyers的几本书。


    在假设"任何行都可以抛出"的情况下,不可能编写异常安全代码。异常安全代码的设计关键依赖于某些合同/保证,这些合同/保证是您应该在代码中期望、观察、遵循和实现的。绝对有必要拥有保证永远不会抛出的代码。还有其他类型的例外保证。

    换句话说,创建异常安全代码在很大程度上是程序设计的问题,而不仅仅是简单编码的问题。


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

    嗯,我当然想。

    • 您确定最后一个"生产就绪"代码是异常安全的吗?

    我确信我使用异常构建的24/7服务器可以24/7运行,并且不会泄漏内存。

    • 你能确定吗?

    很难确定任何代码是否正确。通常,一个人只能通过结果

    • 你知道和/或实际使用其他可行的方法吗?

    不需要。使用异常比我在过去30年中在编程中使用的任何替代方法都更简单、更干净。


    撇开SEH和C++异常之间的混淆,您需要知道在任何时候都可以抛出异常,并将代码写入其中。对异常安全性的需求很大程度上推动了RAII、智能指针和其他现代C++技术的使用。

    如果您遵循良好的模式,那么编写异常安全的代码并不特别困难,事实上,它比在所有情况下正确地编写处理错误返回的代码更容易。


    一般来说,eh是好的。但是C++的实现并不十分友好,因为很难说你的异常捕获覆盖率有多高。例如Java使得这很容易,如果不处理可能的异常,编译器会失败。


    • 你真的写了异常安全代码吗?[没有这样的事。除非您有一个受管理的环境,否则异常是对错误的一种保护。这适用于前三个问题。]

    • 你知道和/或实际使用其他可行的方法吗?[替代什么?这里的问题是人们不能将实际错误与正常程序操作分开。如果它是正常的程序操作(即找不到文件),它就不是真正的错误处理。如果这是一个实际的错误,就没有办法"处理"它,或者它不是一个实际的错误。你的目标是找出哪里出了问题,或者停止电子表格并记录错误,重新启动你的烤面包机的驱动程序,或者祈祷喷气式战斗机可以继续飞行,即使它的软件是小车,并希望最好。]


    是的,我尽力编写异常安全代码。

    这意味着我要注意哪些线可以抛。不是每个人都能做到,记住这一点至关重要。关键是要考虑并设计代码以满足标准中定义的异常保证。

    是否可以编写此操作来提供强异常保证?我必须适应基本的生活吗?哪些行可能会引发异常,我如何确保它们不会损坏对象?


    我们中的一些人喜欢像Java这样的语言,迫使我们声明方法所抛出的所有异常,而不是像C++和C语言那样使它们隐形。

    如果处理得当,异常会优于错误返回代码,如果不是其他原因,您不必手动将失败传播到调用链上。

    也就是说,低级API库编程应该避免异常处理,并坚持错误返回代码。

    我的经验是,在C++中很难编写干净的异常处理代码。我经常使用new(nothrow)


    我非常喜欢使用Eclipse和Java(new to java),因为如果缺少EH处理程序,它会在编辑器中抛出错误。这使得事情很难忘记处理异常…

    另外,使用IDE工具,它会自动添加try/catch块或另一个catch块。


    很多人(我甚至会说大多数人)都这么做。

    异常真正重要的是,如果不编写任何处理代码,那么结果是完全安全且行为良好的。太急于惊慌,但安全。

    您需要主动地在处理程序中犯错误以获得不安全的内容,只有catch(…)将与忽略错误代码进行比较。