你(真的)写exception安全的代码?

exception处理(EH)似乎是当前的标准,通过searchnetworking,我找不到任何尝试改进或取代它的新颖想法或方法(以及存在一些变化,但没有新颖性)。

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

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

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

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

EH取代了旧的清晰的确定性方法(返回值..),它只有一些但可以理解和容易解决的缺点,在代码中创build了许多可能的退出点,如果你开始编写捕获exception的代码被迫做某些事情),那么它甚至通过你的代码创build了许多path(在catch块中的代码,想想你需要日志工具而不是std :: cerr的服务器程序)。 EH有优势,但这不是重点。

我的实际问题:

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

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

回答问题

你真的写exception安全的代码?

我当然是了。

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

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

你确定你的最后一个“生产就绪”的代码是exception安全的吗?

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

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

你不能100%确定你的代码是exception安全的。 但是,那么,你就是用着名的模式来争取它,并且避免众所周知的反模式。

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

在C ++中没有可行的select(例如,您需要恢复到C,避免使用C ++库,以及Windows SEH等外部意外)。

编写exception安全代码

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

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

例外保证

首先要了解的是,您必须能够评估所有function提供的例外保证:

  1. none :你的代码不应该提供这个。 这段代码会泄漏所有的东西,并在抛出的第一个exception处中断。
  2. 基本的 :这是你必须至less提供的保证,也就是说,如果抛出exception,没有资源泄漏,所有的对象仍然是整体
  3. :处理将成功,或抛出一个exception,但如果它抛出,那么数据将处于相同的状态,如果处理还没有开始(这给C ++一个交易电源)
  4. 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 } 

我把所有的代码都写在这个分析的脑海里。

所提供的最低保证是基本的,但是,每条指令的顺序使得整个function“无”,因为如果抛出,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 } 

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

然后,我们创build一个t2的拷贝t2 ,并且从操作4到7对这个拷贝进行处理。如果抛出exception, t2被修改,但是t仍然是原来的。 我们仍然提供强有力的保证。

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

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

现在,提供强有力的保证可能是相当昂贵的,所以不要努力为你的代码提供强有力的保证,但是如果你可以做到这一点没有成本(和C ++内联和其他优化可以使所有的代码以上代价) ,然后做。 function用户会感谢你的。

结论

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

当然,C ++编译器不会提供保证(在我的代码中,我提供了一个@warning doxygen标签),这有点难过,但它不应该阻止你编写exception安全的代码。

正常失败与错误

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

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

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

最后的话

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

当然如此。 有一个“nothrow / nofail”函数知道函数不会失败是一个很大的恩惠。 同样可以说是一个“强”function,它使您能够编写事务性语义的代码,如数据库,提交/回滚function,提交正常执行代码,抛出exception是回滚。

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

所以,据我所知,这值得的。

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

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

  • [我]交换将永远不会失败(甚至不写一个投掷交换)
  • [nobar]这是自定义编写的swap()函数的一个很好的build议。 但是,应该注意的是, std::swap()可能会因内部使用的操作而失败

默认的std::swap会进行复制和分配,对于某些对象可以抛出。 因此,默认的交换可以抛出,用于你的类,甚至STL类。 就C ++标准而言, vectordequelist的交换操作不会抛出,而如果比较函数可以抛出复制构造,则可以map (参见C ++编程语言,特别版,附录E ,E.4.3.Swap )。

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

所以,原文仍然存在:不要写抛出的交换,但是必须记住nobar的注释:确保交换的对象具有非抛出交换。

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

戴夫·亚伯拉罕 ( Dave Abrahams )向我们提供了基本的/强有力的/不重要的保证 ,他在一篇文章中描述了他使STLexception安全的经验:

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

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

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

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

Dionadar指的是下面一行,确实有不确定的行为。

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

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

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

我在考虑Dionadar的意见后,修改了代码。

在C ++中编写exception安全的代码并不是使用大量try {} catch {}块。 这是关于logging你的代码提供什么样的保证。

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

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

  • 基本:当你的代码抛出一个exception,你的代码不会泄漏资源,并且对象保持可破坏状态。
  • 强:当你的代码抛出一个exception,它保持应用程序的状态不变。
  • 没有抛出:你的代码从不抛出exception。

就我个人而言,我发现这些文章相当晚,所以我的C ++代码绝对不是exception安全的。

我们有些人已经使用了超过20年的例外。 PL /我有他们,例如。 他们是一个新的和危险的技术的前提似乎是值得怀疑的。

首先(如Neil所述),SEH是微软的结构化exception处理。 它与C ++中的exception处理类似,但并不相同。 事实上,如果你想在Visual Studio中使用C ++exception处理(Exception Handling),那么默认行为并不能保证本地对象在任何情况下都被销毁! 在这两种情况下,exception处理并不是很难,它只是不同而已。

现在为您的实际问题。

你真的写exception安全的代码?

是。 在任何情况下,我都会争取使用exception安全代码 我传福音使用RAII技术范围访问资源(例如, boost::shared_ptr为内存, boost::lock_guard为locking)。 一般来说, RAII和范围守护技术的一致使用将使exception安全代码更容易编写。 诀窍是了解什么存在以及如何应用它。

你确定你的最后一个“生产就绪”的代码是exception安全的吗?

不,它是一样安全的。 我可以说,我没有看到过程中的错误,因为在24/7的几年活动的例外。 我不期望完美的代码,只是写得很好的代码。 除了提供exception安全性之外,上述技术还能以try / catch块几乎无法实现的方式保证正确性。 如果您捕捉到顶级控制范围(线程,进程等)中的所有内容,那么您可以确定在exception( 大部分时间 )的情况下,您将继续运行。 同样的技术也可以帮助你在面对exception的情况下继续正确运行, 而无需在任何地方使用try / catch

你甚至可以确定它是什么?

是。 你可以肯定通过一个彻底的代码审计,但没有人真的这样做呢? 经常的代码审查和小心的开发人员走了很长的路要走到那里。

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

多年来,我已经尝试了一些变化,比如编码状态(ala HRESULT s )或可怕的setjmp() ... longjmp() 。 这两种方式在实践中都以完全不同的方式分解。


最后,如果你习惯于应用一些技巧,并仔细考虑在哪里可以做些什么来响应exception,那么最终会得到非常易读的代码,这是非常安全的。 您可以按照以下规则进行总结:

  • 你只想看到try / catch当你可以做一些特定的exception
  • 你几乎从不想看到一个原始的newdelete的代码
  • 通常使用std::sprintfsnprintf和数组 – 使用std::ostringstream进行格式化,并用std::vectorstd::stringreplace数组
  • 如果有疑问,请在滚动自己的代码之前查找Boost或STL中的function

我只能build议您学习如何正确使用exception,并且如果您计划使用C ++编写程序,则会忘记结果代码。 如果你想避免例外,你可能要考虑用另一种语言写作,要么没有它们,要么使它们安全 。 如果你真的想学习如何充分利用C ++,请阅读Herb Sutter , Nicolai Josuttis和Scott Meyers的几本书。

在“任何行可以抛出”的假设下编写exception安全的代码是不可能的。 exception安全的代码的devise严重依赖于你应该期待,遵守,遵循和在你的代码中实现的某些契约/保证。 拥有绝对不会丢失的代码是绝对必要的。 那里还有其他种类的例外保证。

换句话说,创buildexception安全的代码在很大程度上是程序devise的问题,而不仅仅是简单的编码问题

  • 你真的写exception安全的代码?

那么,我当然打算。

  • 你确定你的最后一个“生产就绪”的代码是exception安全的吗?

我敢肯定,我使用exception构build的24/7服务器全天候运行,不会泄漏内存。

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

确定任何代码是正确的是非常困难的。 通常情况下,只能通过结果去

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

不。使用exception比过去30年来在编程中使用的任何替代方法都更加简单和容易。

撇开SEH和C ++exception之间的混淆,您需要意识到可以随时抛出exception,并且在编写代码时考虑到这一点。 对exception安全的需求主要是驱动使用RAII,智能指针和其他现代C ++技术的原因。

如果您遵循既定的模式,编写exception安全的代码并不是特别困难,而且实际上比编写在任何情况下都能正确处理错误返回的代码更容易。

一般来说,EH是好的。 但是C ++的实现并不是非常友好,因为很难判断你的exception捕获是否有效。 例如Java使得这很容易,如果你不处理可能的exception,编译器往往会失败。

我真的很喜欢使用Eclipse和Java(Java新手),因为如果缺lessEH处理程序,它会在编辑器中引发错误。 这使得事情很难忘记处理一个例外。

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

是的,我尽我最大的努力去写exception安全的代码。

这意味着我要注意观察哪些线路可以抛出。 不是每个人都可以,而且牢记这一点至关重要。 关键是真的要考虑,并devise你的代码来满足标准中定义的exception保证。

可以写这个操作来提供强有力的例外保证吗? 我必须解决基本问题吗? 哪一行可能会抛出exception,我怎样才能确保如果他们这样做,他们不会破坏对象?

  • 你真的写exception安全的代码? [没有这样的事情。 除非你有一个托pipe的环境,否则例外是错误的纸质屏障。 这适用于前三个问题。]

  • 你知道和/或实际使用替代scheme吗? [替代什么? 这里的问题是人们不把实际的错误与正常的程序操作分开。 如果是正常的程序操作(即没有find文件),这不是真正的error handling。 如果这是一个实际的错误,没有办法“处理”它或它不是一个实际的错误。 您的目标是找出问题所在,并停止电子表格并logging错误,重新启动驱动程序到您的烤面包机,或只是祈求喷气式飞机即使在软件有问题时也能继续飞行,并希望最好。]

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

如果正确完成,exception优于错误返回码,如果没有其他原因,则不需要手动将调用失败传播到调用链。

也就是说,低级的API库编程应该可能避免exception处理,并坚持错误返回码。

这是我的经验,很难用C ++编写干净的exception处理代码。 我最终使用new(nothrow)很多。

很多人(我甚至会说)最多的人都这样做。

对于exception来说真正重要的是,如果你不写任何处理代码 – 结果是非常安全和良好的。 太急于恐慌,但安全。

你需要在处理程序中积极地犯错误来获取不安全的东西,只有catch(…){}会比较忽略错误代码。