您是否(确实)编写了异常安全代码?[关闭]


312

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

尽管大多数人似乎忽略了它或只是接受了它,但是EH 一些巨大的缺点:异常对于代码是不可见的,并且它创建了许多可能的退出点。关于软件的Joel撰写了一篇有关它文章。比较goto合适,让我重新考虑了EH。

我尽量避免使用EH,而只使用返回值,回调或适合目的的任何方法。但是,当您不得不编写可靠的代码时,这些天就不能忽略EH:它以开头new,可能会引发异常,而不是像过去那样仅返回0。这使得任何一行C ++代码都容易受到异常的影响。然后,C ++基本代码中的更多地方会引发异常……std lib会这样做,依此类推。

感觉就像在摇摇欲坠的地面上行走 ..因此,现在我们被迫注意例外!

但是它很难,真的很难。您必须学习编写异常安全代码,即使您有一定的经验,仍然需要仔细检查任何一行代码以确保安全!或者,您开始在各处放置try / catch块,这会使代码混乱,直到达到无法读取的状态为止。

EH替换了旧的干净确定性方法(返回值..),该方法仅具有一些但可以理解且易于解决的缺点,但该方法会在代码中创建许多可能的退出点,并且如果您开始编写捕获异常的代码(您会被迫在某个时候执行),然后它甚至会在您的代码中创建大量路径(catch块中的代码,考虑一个服务器程序,在该服务器程序中您需要除std :: cerr之外的其他日志记录功能。)。EH有优势,但这不是重点。

我的实际问题:

  • 您是否真的编写异常安全代码?
  • 您确定最后一个“生产就绪”代码是异常安全的吗?
  • 您甚至可以确定是吗?
  • 您知道和/或实际使用可行的替代方法吗?

44
“ EH取代了旧的干净确定性方法(返回值。)”什么?异常与返回码一样具有确定性。他们没有什么随机的。
rlbond

2
EH长期以来一直是标准(自Ada以来!)

12
“ EH”是否已建立为异常处理的缩写?我从没看过
Thomas Padron-McCarthy

3
牛顿定律今天适用于大多数事物。几个世纪以来,他们已经预测了非常广泛的现象,这一事实意义重大。
David Thornley,2009年

4
@frunsi:嘿,对不起,让您感到困惑!但是我认为,如果有的话,一个奇怪且无法解释的首字母缩写词或缩写会使人们沮丧的多。
Thomas Padron-McCarthy

Answers:


520

您的问题断言“编写异常安全代码非常困难”。我将首先回答您的问题,然后回答它们背后的隐藏问题。

回答问题

您是否真的编写异常安全代码?

我当然是了。

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

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

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

您甚至可以确定是吗?

编写异常安全代码就像编写无错误代码。

您不能100%确定您的代码是异常安全的。但是,随后您将使用知名的模式并避免使用知名的反模式,为此而努力。

您知道和/或实际使用可行的替代方法吗?

C ++ 中没有可行的替代方法(即,您需要还原到C并避免使用C ++库,以及Windows SEH之类的外部惊喜)。

编写异常安全代码

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

例如,a new可以引发异常,但是分配内置函数(例如int或指针)不会失败。交换永远不会失败(永远不要写std::list::push_back引发交换),可以抛出...

例外保证

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

  1. none:您的代码永远都不能提供该代码。此代码将泄漏所有内容,并在引发第一个异常时分解。
  2. basic:这是您至少必须提供的保证,即,如果引发异常,则不会泄漏任何资源,并且所有对象仍然完整
  3. strong:处理将成功或引发异常,但是如果引发异常,则数据将处于完全未开始处理的状态(这使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
}

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

提供的最低保证是基本的,但是每个指令的排序使整个函数“无”,因为如果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。交换操作应在C ++中不被保留,因此我们希望您编写的交换T内容不被保留(如果不是,则将其重写为不被保留)。

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

现在,提供强有力的保证可能会非常昂贵,因此不要努力为您的所有代码提供强有力的保证,但是如果您可以无偿地做到这一点(C ++内联和其他优化可能会使上面的所有代码变得无成本) ,然后执行。函数用户将感谢您。

结论

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

当然,C ++编译器不会备份保证(在我的代码中,我将保证作为@warning doxygen标记提供),这有点令人遗憾,但是它不应阻止您尝试编写异常安全的代码。

正常故障与错误

程序员如何保证无故障功能将始终成功?毕竟,该功能可能存在错误。

这是真的。异常保证应该由无错误的代码提供。但是,无论哪种语言,调用函数都假定该函数没有错误。没有理智的代码可以防止出现错误。尽力编写代码,然后假设它没有错误,从而提供保证。如果有错误,请更正。

异常是由于异常的处理失败,而不是代码错误。

最后的话

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

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

然后,“基本”是您应该提供的最起码的保证。C ++是一种非常强大的语言,它具有范围,可以避免任何资源泄漏(垃圾收集器会发现很难为数据库,连接或文件句柄提供某种东西)。

因此,据我所知,这值得的。

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

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

  • [我]交换将永远不会失败(甚至不写引发交换)
  • [nobar]对于自定义编写的swap()功能,这是一个很好的建议。但是,应注意的是,它可能会因其std::swap()内部使用的操作而失败

默认情况下std::swap将制作副本和分配,对于某些对象,这些副本和分配可以抛出。因此,默认的交换可能会抛出,或者用于您的类,甚至用于STL类。至于C ++标准而言,对于交换操作vectordequelist不会抛出,而它可以用于map如果比较仿函数可以拷贝构造(见抛出C ++编程语言,特别版,附录E,E.4.3交换)。

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

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

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

给我们提供基本/强/无担保的Dave Abrahams在一篇文章中描述了他使STL异常安全的经验:

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

看一下第七点(异常安全的自动测试),他依靠自动单元测试来确保对每个案例都进行了测试。我想这部分是问题作者“ 您什至可以确定,是吗? ” 的绝佳答案。

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

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

Dionadar指的是以下行,该行确实具有未定义的行为。

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

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

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

我改正了Dionadar的注释后的代码。


6
非常感谢!我仍然希望有所不同。但是,我接受您坚持使用C ++并进行详细说明的答案。您甚至反驳了我的“这使任何C ++代码行都容易受到异常的影响”的冒犯。毕竟,逐行分析是有意义的……我必须考虑一下。
弗隆西(Frunsi)

8
“交换将永远不会失败”。对于自定义编写的swap()函数,这是一个很好的建议。但是,应注意,std :: swap()可能会基于其内部使用的操作而失败。
nobar 2010年

7
@Mehrdad::"finally" does exactly what you were trying to achieve当然可以。并且由于使用容易产生易碎的代码finally,因此“尝试使用资源”的概念最终在Java 7中引入(即,在C#之后的10年using和在C ++析构函数的30年之后)。我批评这种脆性。至于Just because [finally] doesn't match your taste (RAII doesn't match mine, [...]) doesn't mean it's "failing":在这个方面,业界不喜欢您的口味,因为垃圾收集语言倾向于添加RAII启发式的语句(C#using和Java try)。
paercebal 2012年

5
@Mehrdad::RAII doesn't match mine, since it needs a new struct every single darn time, which is tedious sometimes不,不是。您可以使用智能指针或实用程序类来“保护”资源。
paercebal

5
@Mehrdad:“它强迫您为可能不需要包装的东西做包装”-以RAII方式编码时,您将不需要任何包装:资源是对象,对象生命周期是资源生命周期。但是,我来自C ++世界,目前正在与Java项目打交道。与C ++中的RAII相比,Java中的“手动资源管理”简直就是一个必备!我的看法是,但是Java很早以前就将“自动内存mgmt”换成了“自动资源mgmt”。
弗伦西(Frunsi)

32

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

我建议阅读Herb Sutter的“本周大师”系列,尤其是第59、60和61期。

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

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

我个人很晚才发现这些文章,因此我的许多C ++代码绝对不是异常安全的。


1
他的书“ Exceptional C ++”也很不错。但我仍然尝试质疑EH的概念...
Frunsi

8
+1 OP以异常安全性(通常是关于RAII的更多信息)联合处理异常(捕获它们)
jk。

我怀疑很少有生产C ++代码是异常安全的。
拉德瓦尔德

18

我们中有些人使用异常已有20多年了。例如,PL / I有它们。对我来说,它们是一种新的危险技术的前提似乎令人怀疑。


请不要误会我的意思,我正在(或试图)质疑EH。特别是C ++ EH。我正在寻找替代品。也许我必须接受它(如果可能的话,我会接受),但是我认为可能会有更好的选择。它不是,我认为这个概念是新的,但是,是的,我认为它可以比明确的错误处理更危险的返回码...
Frunsi

1
如果您不喜欢它,请不要使用它。将尝试块放在可能引发的需要调用的事物周围,重新发明旧的错误代码,并忍受它所存在的问题,在某些情况下是可以忍受的。
bmargulies

好的,完美,我将只使用EH和错误代码,并继续使用它。我是一个机智的人,我应该自己解决这个问题!;)
Frunsi

17

首先(如Neil所述),SEH是Microsoft的结构化异常处理。它类似于但不等同于C ++中的异常处理。实际上,如果要在Visual Studio中使用它,则必须启用C ++异常处理 -默认行为不能保证在所有情况下都销毁本地对象!在这两种情况下,异常处理并不,只是有所不同

现在是您的实际问题。

您是否真的编写异常安全代码?

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

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

不。它是如此安全。我可以说由于几年24/7活动中的异常,我还没有看到过过程错误。我不希望有完美的代码,而只是编写良好的代码。除了提供异常安全性之外,上述技术还以try/ catch块几乎无法实现的方式保证了正确性。如果您正在捕获顶级控件范围中的所有内容(线程,进程等),则可以确保在大多数情况下面对异常继续运行。相同的技术也将帮助您在无处/ 阻止的情况继续正确运行trycatch

您甚至可以确定是吗?

是。您可以通过全面的代码审核来确保,但没有人真正做到这一点吗?定期的代码审查和谨慎的开发人员可以走很长一段路。

您知道和/或实际使用可行的替代方法吗?

多年来,我尝试了一些变体,例如以高位(ala HRESULTs)编码状态或可怕的setjmp() ... longjmp()骇客。尽管实际上这两种方式完全不同,但它们在实践中均会分解。


最后,如果您习惯于应用一些技术并仔细考虑可以在哪里实际执行某些操作以响应异常,那么您将得到非常可读的,安全的异常代码。您可以按照以下规则进行总结:

  • 您只想查看try/ catch何时可以对特定异常进行处理
  • 您几乎永远都不想看到原始的 newdelete代码或代码
  • eschew std::sprintfsnprintf和数组,通常std::ostringstream用于格式化和替换数组std::vectorstd::string
  • 如有疑问,请在滚动之前先查找Boost或STL中的功能

我只能建议您学习如何正确使用异常,而如果打算使用C ++编写,则请忽略结果代码。如果你想避免异常,你可能要考虑用另一种语言编写,要么没有他们使他们的安全。如果您想真正学习如何充分利用C ++,请阅读Herb SutterNicolai JosuttisScott Meyers的几本书。


“默认行为不能保证在所有情况下都销毁本地对象”是说Visual Studio C ++编译器的默认设置生成的代码在遇到异常时是不正确的。真的是这样吗?
拉德瓦尔德

“您几乎永远都不希望看到原始代码newdelete代码”:原始的我想您的意思是在构造函数或析构函数之外。
拉德瓦尔德

@Raedwald-re:VC ++:抛出SEH异常时,VS2005版本的VC ++不会破坏本地对象。阅读“启用C ++异常处理”。在VS2005中,默认情况下,SEH异常不会调用C ++对象的析构函数。如果调用Win32函数或C接口DLL中定义的任何函数,则您必须为此担心,因为它们可以(有时会)以您的方式抛出SEH异常。
D.Shawley 2011年

@Raedwald:re:raw:基本上,delete永远不要在实现tr1::shared_ptr之类的外部使用。 new可以使用,前提是它的用法类似于tr1::shared_ptr<X> ptr(new X(arg, arg));。重要的是,的结果new直接进入托管指针。在上页boost::shared_ptr最佳实践介绍最好的。
D.Shawley 2011年

为什么在您的异常安全规则集中引用std :: sprintf(et al)?这些文件并没有说明它们引发了任何异常,例如,en.cppreference.com
w /

10

在“任何行都可以抛出”的假设下,不可能编写异常安全代码。异常安全代码的设计主要取决于您应该在代码中期望,遵守,遵循和实现的某些合同/保证。绝对需要保证永不抛出的代码。还有其他种类的例外保证。

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


8
  • 您是否真的编写异常安全代码?

好吧,我当然打算。

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

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

  • 您甚至可以确定是吗?

确保任何代码正确都是非常困难的。通常,只能靠结果

  • 您知道和/或实际使用可行的替代方法吗?

否。使用异常比我过去30年来在编程中使用的任何替代方法都更加简洁明了。


30
这个答案没有价值。
马特·乔纳

6
@MattJoiner然后,这个问题没有价值。
Miles Rout

5

除了SEH和C ++异常之间的混乱之外,您需要意识到可以随时抛出异常,并牢记这一点来编写代码。对异常安全的需求在很大程度上推动了RAII,智能指针和其他现代C ++技术的使用。

如果遵循公认的模式,编写异常安全代码并不是特别困难,实际上,这比编写可在所有情况下正确处理错误返回的代码容易。


3

一般来说,EH是好的。但是C ++的实现不是很友好,因为很难判断异常捕获的覆盖范围。举例来说,Java可以轻松实现这一点,如果您不处理可能的异常,则编译器往往会失败。


2
明智地使用会更好noexcept
einpoklum

2

我真的很喜欢使用Eclipse和Java(Java的新功能),因为如果缺少EH处理程序,它将在编辑器中引发错误。这使得很多事情很难忘记处理异常。

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


5
那就是checked(Java)和unchecked(c ++)异常之间的区别(Java也有一些例外)。检查异常的优点是您编写的内容,但是它有其自身的缺点。Google针对不同的方法以及它们所存在的不同问题。
大卫·罗德里格斯(DavidRodríguez)-dribeas,2009年

2

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

如果处理得当,则异常的产生要优于错误返回码,除非您不必手动将故障传播到整个调用链中,否则异常就比错误返回码更好。

话虽如此,低级API库编程可能应该避免异常处理,并坚持错误返回代码。

根据我的经验,很难用C ++编写干净的异常处理代码。我最终使用new(nothrow)了很多东西。


4
而且您也避免使用大多数标准库?仅使用new(std::nothrow)是不够的。顺便说一下,用C ++编写异常安全代码比用Java编写更容易:en.wikipedia.org/wiki/Resource_Acquisition_Is_Initialization
avakar

3
Java检查的异常可用性非常高。实际上,在非Java语言中,它们并不是成功的语言。这就是为什么现在认为C ++中的“ throw”语句已过时,而C#从未认真考虑过实现它们(这是一种设计选择)。对于Java,以下文档可能会有所启发:googletesting.blogspot.com/2009/09/…– paercebal
2009年

2
根据我的经验,用C ++编写异常安全代码并不难,并且通常会导致代码更简洁。当然,您必须学习做到这一点。
David Thornley,2009年

2

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

这意味着我照顾,以保持眼睛上的行会抛出。并不是每个人都可以,记住这一点至关重要。关键是真正考虑并设计您的代码以满足标准中定义的异常保证。

可以编写此操作以提供强大的异常保证吗?我必须满足基本要求吗?哪些行可能会引发异常,并且我如何确保它们不会破坏对象?


2
  • 您是否真的编写异常安全代码?[没有这样的东西。除非您拥有托管环境,否则异常是防止错误的纸面盾牌。这适用于前三个问题。]

  • 您知道和/或实际使用可行的替代方法吗?[替代什么?这里的问题是人们没有将实际错误与正常程序操作区分开。如果它是正常的程序操作(即找不到文件),则实际上不是错误处理。如果是实际错误,则无法对其进行“处理”,或者这不是实际错误。您在这里的目标是找出问题所在,然后停止电子表格并记录错误,将驱动程序重新启动到烤面包机,或者只是祈祷Jetfighter即使软件有故障,也可以继续飞行,并希望最好。


0

很多人(我什至会说最多)。

异常真正重要的是,如果您不编写任何处理代码,则结果是绝对安全且行为良好的。太急于恐慌,但是很安全。

您需要在处理程序中主动犯错以获取不安全的内容,并且只有catch(...){}会与忽略错误代码进行比较。


4
不对。编写非异常安全的代码非常容易。例如:f = new foo(); f->doSomething(); delete f;如果doSomething方法引发异常,则内存泄漏。
克里斯托弗·约翰逊

1
程序何时终止并不重要,对吧?要继续执行,您必须积极吞下异常。好吧,在某些特定情况下,不清理终止仍然是不可接受的,但是在任何编程语言和风格下,都需要特别注意。
ima

在C ++和托管代码中,您都不能忽略异常(也不能编写处理代码)。这将是不安全的,而且行为也不正常。除了一些玩具代码。
弗隆西(Frunsi)

1
如果您忽略应用程序代码中的异常,则在涉及外部资源时,该程序仍可能无法正常运行。的确,操作系统关心的是关闭文件句柄,锁,套接字等。但是并非所有内容都得到处理,例如,在写入文件时可能会留下不必要的文件或损坏文件等。如果您忽略异常,那么您在Java中会遇到问题,在C ++中,您可以使用RAII(但是当您使用RAII技术时,您很可能会使用它们,因为关心异常)...
Frunsi

3
拜托,别误会我的话。我写了“如果您不写任何处理代码”,我就是这么想的。要忽略异常,您需要编写代码。
ima
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.