异常处理(EH)似乎是当前的标准,通过搜索网络,我找不到任何新的想法或方法来改进或取代它(好吧,存在一些变化,但没有什么新奇的)。

尽管大多数人似乎忽略了它或只是接受它,EH有一些巨大的缺点:异常对代码是不可见的,它创建了许多许多可能的退出点。软件的Joel写了一篇关于它的文章。与goto的对比非常完美,这让我再次思考EH。

我尽量避免EH,只使用返回值,回调或任何符合目的的东西。但是,当您必须编写可靠的代码时,现在就不能忽略EH了:它从new开始,可能会抛出异常,而不只是返回0(就像以前一样)。这使得几乎每一行c++代码都容易出现异常。然后在c++基本代码中抛出异常的地方更多……STD lib可以这样做,等等。

这感觉就像在摇摇欲坠的地面上行走。因此,现在我们不得不关注异常!

但这很难,真的很难。你必须学会编写异常安全的代码,即使你有一些这方面的经验,仍然需要仔细检查每一行代码的安全性!或者您开始到处放置try/catch块,这会使代码变得混乱,直到达到不可读的状态。

EH replaced the old clean deterministical approach (return values..), which had just a few but understandable and easily solveable drawbacks with an approach that creates many possible exit points in your code, and if you start writing code that catches exceptions (what you are forced to do at some point), then it even creates a multitude of paths through your code (code in the catch blocks, think about a server program where you need logging facilities other than std::cerr ..). EH has advantages, but that's not the point.

我真正的问题是:

你真的写异常安全的代码吗? 您确定最后的“生产就绪”代码是异常安全的吗? 你能确定这是真的吗? 你知道和/或实际上使用有效的替代方法吗?


当前回答

首先(正如Neil所说),SEH是微软的结构化异常处理。它类似于c++中的异常处理,但不完全相同。事实上,如果你想在Visual Studio中使用它,你必须启用c++异常处理——默认行为并不能保证在所有情况下局部对象都被销毁!在任何一种情况下,异常处理并不难,只是不同而已。

现在轮到你的实际问题了。

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

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

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

No. It is as safe as it is. I can say that I haven't seen a process fault due to an exception in several years of 24/7 activity. I don't expect perfect code, just well-written code. In addition to providing exception safety, the techniques above guarantee correctness in a way that is near impossible to achieve with try/catch blocks. If you are catching everything in your top control scope (thread, process, etc.), then you can be sure that you will continue to run in the face of exceptions (most of the time). The same techniques will also help you continue to run correctly in the face of exceptions without try/catch blocks everywhere.

你能确定这是真的吗?

是的。你可以通过彻底的代码审核来确定,但没有人真的这么做,不是吗?不过,定期的代码审查和细心的开发人员要达到这个目标还有很长的路要走。

你知道和/或实际上使用有效的替代方法吗?

多年来,我尝试了一些变化,比如在上面的位编码状态(ala HRESULTs)或可怕的setjmp()…longjmp()攻击。这两种方法在实践中都是行不通的,尽管是以完全不同的方式。


最后,如果您养成了应用一些技术的习惯,并仔细考虑在哪里可以实际执行一些响应异常的操作,那么您最终将得到非常可读且异常安全的代码。你可以通过以下规则来总结:

只有当您可以对特定异常执行某些操作时,才希望看到try/catch 您几乎不希望在代码中看到原始的new或delete 一般避免使用std::sprintf、snprintf和数组——使用std::ostringstream进行格式化,并使用std::vector和std::string替换数组 如果有疑问,在开发自己的Boost或STL之前,先看看它的功能

我只能建议你学习如何正确地使用异常,如果你打算用c++写代码,忘记结果代码。如果您想要避免异常,您可能会考虑使用另一种语言来编写,这种语言要么没有异常,要么使异常安全。如果你真的想学习如何充分利用c++,可以阅读一些Herb Sutter、Nicolai Josuttis和Scott Meyers的书。

其他回答

首先(正如Neil所说),SEH是微软的结构化异常处理。它类似于c++中的异常处理,但不完全相同。事实上,如果你想在Visual Studio中使用它,你必须启用c++异常处理——默认行为并不能保证在所有情况下局部对象都被销毁!在任何一种情况下,异常处理并不难,只是不同而已。

现在轮到你的实际问题了。

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

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

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

No. It is as safe as it is. I can say that I haven't seen a process fault due to an exception in several years of 24/7 activity. I don't expect perfect code, just well-written code. In addition to providing exception safety, the techniques above guarantee correctness in a way that is near impossible to achieve with try/catch blocks. If you are catching everything in your top control scope (thread, process, etc.), then you can be sure that you will continue to run in the face of exceptions (most of the time). The same techniques will also help you continue to run correctly in the face of exceptions without try/catch blocks everywhere.

你能确定这是真的吗?

是的。你可以通过彻底的代码审核来确定,但没有人真的这么做,不是吗?不过,定期的代码审查和细心的开发人员要达到这个目标还有很长的路要走。

你知道和/或实际上使用有效的替代方法吗?

多年来,我尝试了一些变化,比如在上面的位编码状态(ala HRESULTs)或可怕的setjmp()…longjmp()攻击。这两种方法在实践中都是行不通的,尽管是以完全不同的方式。


最后,如果您养成了应用一些技术的习惯,并仔细考虑在哪里可以实际执行一些响应异常的操作,那么您最终将得到非常可读且异常安全的代码。你可以通过以下规则来总结:

只有当您可以对特定异常执行某些操作时,才希望看到try/catch 您几乎不希望在代码中看到原始的new或delete 一般避免使用std::sprintf、snprintf和数组——使用std::ostringstream进行格式化,并使用std::vector和std::string替换数组 如果有疑问,在开发自己的Boost或STL之前,先看看它的功能

我只能建议你学习如何正确地使用异常,如果你打算用c++写代码,忘记结果代码。如果您想要避免异常,您可能会考虑使用另一种语言来编写,这种语言要么没有异常,要么使异常安全。如果你真的想学习如何充分利用c++,可以阅读一些Herb Sutter、Nicolai Josuttis和Scott Meyers的书。

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

我推荐大家阅读Herb Sutter的《每周大师》系列,特别是第59、60和61期。

总之,你可以提供三种级别的异常安全:

基本:当您的代码抛出异常时,您的代码不会泄漏资源,对象仍然是可破坏的。 强:当您的代码抛出异常时,它将保持应用程序的状态不变。 不抛出:你的代码永远不会抛出异常。

就我个人而言,我是很晚才发现这些文章的,所以我的很多c++代码肯定不是异常安全的。

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

如果处理得当,异常优于错误返回码,如果没有其他原因,只是因为您不需要手动在调用链中传播失败。

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

根据我的经验,在c++中很难编写干净的异常处理代码。最后我经常使用new(nothrow)。

不过,我真的很喜欢使用Eclipse和Java (Java新手),因为如果缺少EH处理程序,它会在编辑器中抛出错误。这使得忘记处理异常变得更加困难……

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

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

回答问题

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

我当然知道。

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

事实上,当您需要使用STL或Boost代码时,这是必要的。例如,c++线程(boost::thread或std::thread)将抛出异常以优雅地退出。

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

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

你不能100%确定你的代码是异常安全的。但是,您要努力做到这一点,使用众所周知的模式,避免众所周知的反模式。

你知道和/或实际上使用有效的替代方法吗?

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

编写异常安全代码

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

例如,new可以抛出异常,但赋值内置对象(例如int或指针)不会失败。交换永远不会失败(不要写抛出交换),std::list::push_back可以抛出…

除了保证

首先要理解的是,你必须能够计算所有函数提供的异常保证:

none: Your code should never offer that. This code will leak everything, and break down at the very first exception thrown. basic: This is the guarantee you must at the very least offer, that is, if an exception is thrown, no resources are leaked, and all objects are still whole strong: The processing will either succeed, or throw an exception, but if it throws, then the data will be in the same state as if the processing had not started at all (this gives a transactional power to C++) nothrow/nofail: The processing will succeed.

代码示例

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

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
}

我在编写所有代码时都考虑到这种分析。

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

首先要做的是将函数设置为“basic”,即将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也不会泄漏,因为它属于智能指针。

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

然后我们交换t和t2。交换操作在c++中应该是nothrow的,所以让我们希望你为T写的交换是nothrow的(如果不是,重写它,使它是nothrow的)。

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

现在,提供强保证可能是相当昂贵的,所以不要努力为您的所有代码提供强保证,但如果您可以做到这一点而不花费成本(并且c++内联和其他优化可以使上述所有代码都不花费成本),那么就这样做。函数用户会为此感谢你。

结论

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

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

正常故障vs. bug

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

这是真的。异常保证应该由无bug的代码提供。但是,在任何语言中,调用函数都假定该函数没有错误。任何理智的代码都不能保护自己不出现错误。尽你所能写出最好的代码,然后,假设它是无错误的,并提供保证。如果有bug,就改正它。

异常是针对异常处理失败,而不是针对代码错误。

最后一句话

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

当然是这样。拥有一个“nothrow/no-fail”函数,并且知道该函数不会失败是一个很大的福音。“强”函数也可以这样说,它允许您编写具有事务语义的代码,如数据库,具有提交/回滚特性,提交是代码的正常执行,抛出异常是回滚。

那么,“基本”是你应该提供的最起码的保证。c++是一种非常强大的语言,它的作用域使您能够避免任何资源泄漏(垃圾收集器将很难为数据库、连接或文件句柄提供这种泄漏)。

所以,在我看来,这是值得的。

编辑2010-01-29:关于非抛出交换

nobar做了一个评论,我相信,是相当相关的,因为它是“如何编写异常安全代码”的一部分:

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

默认的std::swap将生成副本和赋值,对于某些对象,可以抛出。因此,默认交换可能会抛出,用于您的类,甚至用于STL类。就c++标准而言,vector、deque和list的交换操作不会抛出,而如果比较函数可以在复制构造时抛出,则map的交换操作可以抛出(参见c++编程语言特别版,附录E, E. 4.3.swap)。

查看Visual c++ 2008对vector交换的实现,如果两个vector具有相同的分配器(即正常情况),则vector交换不会抛出,但如果它们具有不同的分配器,则会生成副本。因此,我假设它会在最后一种情况下出现。

因此,原始文本仍然有效:永远不要编写抛出交换,但必须记住nobar的注释:确保您正在交换的对象具有非抛出交换。

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

Dave Abrahams为我们提供了basic/strong/nothrow保证,他在一篇文章中描述了他使STL异常安全的经验:

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

看看第7点(异常安全的自动化测试),他依赖于自动化单元测试来确保每个用例都经过了测试。我想这部分是对作者问题“你能确定吗?”的一个很好的回答。

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

T.integer += 1;是没有保证溢出不会发生not异常安全,实际上可能在技术上调用UB!(有符号溢出是UB: c++ 11 5/4 "如果在表达式求值期间,结果不是数学上定义的,或者不在其类型的可表示值范围内,则行为未定义。")注意无符号整数不会溢出,而是在模2^#位的等价类中进行计算。

Dionadar指的是下面这行,它确实具有未定义的行为。

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

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

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

我根据Dionadar的注释修正了代码。