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

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

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

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

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

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

嗯取代旧的清洁deterministical方法(返回值. .),有几个但理解和容易solveable缺点的方法在代码中创建了许多可能的退出点,如果你开始编写代码捕获异常(你被迫做什么在某种程度上),那么它甚至创造了众多的路径通过代码(代码捕获块,想想一个服务器程序,您需要日志设施除了std:: cerr . .)。EH有优势,但这不是重点。

我真正的问题是:

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

我们中的一些人使用例外已经超过20年了。例如PL/I就有。在我看来,它们是一种危险的新技术的前提是值得怀疑的。

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

嗯,我当然打算。

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

我确信我使用异常构建的24/7服务器全天候运行,不会泄漏内存。

  • 你能确定这是真的吗?

很难确定任何代码都是正确的。通常情况下,人们只能根据结果来判断

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

不。使用异常比我过去30年在编程中使用过的任何替代方法都更简洁、更容易。

先不考虑SEH和c++异常之间的混淆,您需要意识到异常可能在任何时候抛出,并在编写代码时牢记这一点。异常安全的需求在很大程度上推动了RAII、智能指针和其他现代c++技术的使用。

如果遵循公认的模式,编写异常安全的代码就不是特别困难,事实上,它比编写在所有情况下都能正确处理错误返回的代码要容易得多。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

你能确定这是真的吗?

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

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

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


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

  • 当你可以对特定的异常做一些事情时,你只希望看到try/catch
  • 你几乎不希望在代码中看到一个原始的newdelete
  • 一般避免使用std::sprintfsnprintf和数组——使用std::ostringstream进行格式化,并用std::vectorstd::string替换数组
  • 如果有疑问,在开发自己的Boost或STL之前,先看看它的功能

我只能建议你学习如何正确地使用异常,如果你打算用c++写代码,忘记结果代码。如果你想避免异常,你可能会考虑用另一种语言没有它们让他们安全来编写。如果你真的想学习如何充分利用c++,请阅读一些来自Herb Sutter尼科莱Josuttis斯科特•迈耶斯的书籍。

如果假设“任何行都可以抛出”,就不可能编写异常安全的代码。异常安全代码的设计主要依赖于某些契约/保证,你应该在你的代码中期望、观察、遵循和实现这些契约/保证。绝对有必要有保证从来没有抛出的代码。还有其他类型的异常保证。

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

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

回答问题

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

我当然知道。

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

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

你确定你的最后一批产品准备好了吗?代码是异常安全的?

你能确定这是真的吗?

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

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

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

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

编写异常安全代码

要编写异常安全的代码,你必须知道第一个你所编写的每条指令的异常安全级别。

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

除了保证

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

  1. 没有一个:你的代码不应该提供这个。这段代码将泄露所有内容,并在抛出第一个异常时崩溃。
  2. 基本:这是你必须至少提供的保证,也就是说,如果抛出异常,没有资源泄漏,所有对象仍然是完整的
  3. <强大的>强大的:处理要么成功,要么抛出异常,但如果抛出异常,则数据将处于与处理根本没有开始相同的状态(这为c++提供了事务能力)
  4. nothrow / nofail:处理将成功。

代码示例

下面的代码看起来像正确的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
}

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

提供的最低保证是基本的,但是,每条指令的顺序使得整个函数“无”,因为如果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仍然是原始的。我们仍然提供强有力的保证。

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

因此,如果到达函数的末尾,则一切都成功(不需要返回类型),并且t有其例外值。如果失败,则t仍保留其原始值。

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

结论

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

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

正常故障vs. bug

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

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

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

最后一句话

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

当然是这样。“没有失败”;函数知道函数不会失败是一个很大的福音。同样的道理也适用于“strong”;函数,它使您能够编写具有事务语义的代码,就像数据库一样,具有提交/回滚特性,提交是代码的正常执行,抛出异常是回滚。

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

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

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

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

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

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

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

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

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

戴夫·亚伯拉罕,他给了我们基本/强/ nothrow担保,在一篇文章中描述了他关于使STL异常安全的经验:

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

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

编辑2013-05-31:Comment from 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的注释修正了代码。

是的,我尽了最大的努力来编写异常安全的代码。

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

这个操作可以编写为提供强异常保证吗?我必须满足于最基本的吗?哪些行可能抛出异常,我如何确保如果它们这样做,它们不会破坏对象?

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

关于异常,真正重要的是,如果你不写任何处理代码,结果是完全安全和良好的。太急于慌张,反而安全。

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

  • 你真的写异常安全代码吗? 没有这样的事。除非您有一个受管理的环境,否则异常是错误的纸面盾牌。

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

一般来说,EH是好的。但是c++的实现不是很友好,因为很难判断你的异常捕获覆盖率有多好。例如,Java使这很容易,如果你不处理可能的异常,编译器将倾向于失败。