繁体   English   中英

你(真的)编写异常安全代码吗? [关闭]

[英]Do you (really) write exception safe code? [closed]

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

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

我尽量避免使用EH,只使用返回值,回调或任何适合目的的东西。 但是当你必须编写可靠的代码时,你现在就不能忽视EH :它从new开始,它可能抛出一个异常,而不是只返回0(就像过去一样)。 这使得任何C ++代码行都容易受到异常的影响。 然后C ++基础代码中的更多地方抛出异常...... std lib执行它,依此类推。

这感觉就像走在摇摇欲坠的地面上 。所以,现在我们被迫关注异常!

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

EH取代了旧的干净确定性方法(返回值..),它只有一些但可以理解且易于解决的缺点,一种方法可以在代码中创建许多可能的退出点,并且如果你开始编写捕获异常的代码(你是什么的)在某些时候被迫做某事),然后它甚至通过你的代码创建了许多路径(catch块中的代码,考虑一个服务器程序,你需要除了std :: cerr之外的日志工具..)。 EH有优势,但这不是重点。

我的实际问题:

  • 你真的写异常安全代码吗?
  • 您确定最后一个“生产就绪”代码是异常安全的吗?
  • 你能确定吗,它是吗?
  • 你知道和/或实际使用有效的替代品吗?

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

回答问题

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

我当然是了。

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

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

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

你能确定吗,它是吗?

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

您不能100%确定您的代码是异常安全的。 但是,然后,你使用众所周知的模式,并避免众所周知的反模式来争取它。

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

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

编写异常安全代码

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

例如, 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不会泄漏,因为它由智能指针拥有。

然后,我们创建t的副本t2 ,并从操作4到7处理此副本。如果抛出某些内容,则修改t2 ,但是, t仍然是原始的。 我们仍然提供强有力的保证。

然后,我们交换tt2 交换操作不应该在C ++中进行,所以我们希望你为T编写的交换是不可靠的(如果不是,则重写它以便它不是)。

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

现在,提供强有力的保证可能会非常昂贵,所以不要努力为您的所有代码提供强有力的保证,但如果您可以不花费成本(并且C ++内联和其他优化可以使所有代码无成本) ,然后去做。 功能用户会感谢你。

结论

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

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

正常失败与错误

程序员如何保证无故障功能永远成功? 毕竟,该功能可能有一个错误。

这是真的。 异常保证应该由无错误的代码提供。 但是,在任何语言中,调用函数都假定函数没有错误。 没有合理的代码可以保护自己不受错误的影响。 尽可能地编写代码,然后提供保证,假设它没有错误。 如果有错误,请更正错误。

例外情况是异常处理失败,而不是代码错误。

最后的话

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

当然如此。 知道函数不会失败的“nothrow / no-fail”函数是一个很大的好处。 对于“强”函数也是如此,它使您能够使用事务语义编写代码,如数据库,具有提交/回滚功能,提交是代码的正常执行,抛出异常是回滚。

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

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

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

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

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

默认的std::swap将进行复制和赋值,对于某些对象,它们可以抛出。 因此,默认交换可以抛出,既可用于您的类,也可用于STL类。 ). 就C ++标准而言, vectordequelist的交换操作不会抛出,而如果比较functor可以抛出复制结构,它可以用于map (参见 )。

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

所以,原始文本仍然存在:不要写一个抛出交换,但必须记住nobar的注释:确保你交换的对象有一个非投掷交换。

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

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

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

请看第7点(异常安全的自动化测试),他依靠自动化单元测试来确保每个案例都经过测试。 ". 我想这部分是对作者的问题的一个很好的答案:“ ”。

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

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

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

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

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

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

我用Dionadar的评论更正了代码。

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

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

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

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

就个人而言,我发现这些文章的时间很晚,因此我的C ++代码绝对不是例外。

我们中的一些人已经使用例外超过20年。 例如,PL / I有它们。 它们是一种新的危险技术的前提似乎对我来说是个问题。

首先(正如Neil所说),SEH是微软的结构化异常处理。 它与C ++中的异常处理类似但不完全相同。 实际上,如果在Visual Studio中需要它,则必须启用C ++异常处理 - 默认行为不保证在所有情况下都会销毁本地对象! 在任何一种情况下,异常处理并不是真的更难,它只是不同

现在为您提出实际问题。

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

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

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

不,这是安全的。 我可以说,由于几年的24/7活动中的异常,我没有看到过程故障。 我不希望完美的代码,只是编写良好的代码。 除了提供异常安全性之外,上述技术还保证了使用try / catch块几乎无法实现的正确性。 如果您正在捕获最高控制范围(线程,进程等)中的所有内容,那么您可以确保在面对异常( 大多数情况下 )时继续运行。 同样的技术也将帮助你继续正确例外的脸上没有运行try / catch块随处可见

你能确定它是吗?

是。 您可以通过彻底的代码审核来确定,但没有人真正这样做吗? 定期的代码审查和细心的开发人员在很长的路要走。

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

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


最后,如果您习惯于应用一些技巧并仔细考虑在异常情况下实际可以执行某些操作的位置,那么最终会得到非常易读且异常安全的代码。 您可以按照以下规则进行总结:

  • 您只想在可以对特定异常执行某些操作时查看try / catch
  • 你几乎不想在代码中看到原始的newdelete
  • Eschew std::sprintfsnprintf和一般数组 - 使用std::ostringstream格式化并用std::vectorstd::string替换数组
  • 如有疑问,请在推出自己的功能之前在Boost或STL中查找功能

我只能建议您学习如何正确使用异常,如果计划用C ++编写,请忘记结果代码。 如果您想避免异常,您可能需要考虑用另一种语言编写,或者使用它们,或者使它们安全 如果你想真正学习如何充分利用C ++,请阅读Herb SutterNicolai JosuttisScott Meyers的一些书。

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

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

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

好吧,我当然打算。

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

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

  • 你能确定吗,它是吗?

很难确定任何代码是否正确。 通常,人们只能按结果去做

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

没有。使用例外比我在编程过去30年中使用的任何替代方案更清晰,更容易。

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

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

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

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

此外,使用IDE工具,它会自动添加try / catch块或其他catch块。

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

如果正确完成,异常优于错误返回代码,如果没有其他原因,您不必手动向上传播失败的调用链。

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

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

我尝试了最好的编写异常安全的代码,是的。

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

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

  • 你真的写异常安全代码吗? [没有这样的事情。 除非您拥有托管环境,否则例外是对错误的纸张屏障。 这适用于前三个问题。]

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

人们做了很多(我甚至会说最多)。

对于异常而言,真正重要的是,如果您不编写任何处理代码 - 结果非常安全且表现良好。 太急于恐慌,但安全。

你需要在处理程序中积极地犯错以获得不安全的东西,并且只有catch(...){}将与忽略错误代码进行比较。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM