破例

Diego Dagum 几周前,C++异常发生了一场有趣的争论。 C++ MVPS 讨论列表。导火索是一种无辜而具体的东西 你们能扔吗 std::运行时异常 或者定义一个新的异常层次结构?你对这件事有什么看法 ATLTROW公司 / 亚特兰蒂斯Win32 ? 但正如它通常发生的那样,回复激发了新的问题、理由等,从而转移了观点,并展示了这个糟糕的软件行业中另一个不可能找到“圣杯”的例子,即 一刀切 回答任何可能的问题(当前或未来)。在本例中,我们将讨论异常生成及其后续处理。尽管如此,这些论据是如此丰富,我决定汇编一些,并在这里与更广泛的社区分享。

null

简介

异常生成和处理的最佳实践在支持异常的任何编程语言中都是一个有争议的问题,尽管可能在C++的情况下,它的复杂性由于其第一版本中没有出现异常而加重,并且在那时之前出现了一些替代方法(有些人可能会记住)。 旧的、基于宏的MFC异常模型 ). 此外,C++可能是跨开发者的最广泛使用的语言,因为它是由java和/或.NET开发人员代知的,并且是目前在java和C语言之前最广泛使用的教学语言。 C++仍然是最开放的工作之一 使大型C++项目由不同背景和方法的开发人员构建,这在整个异常处理和开发中都是可能的。

异常层次结构

最初提到的问题 std::运行时错误 作为自定义层次结构的潜在根,但有人更喜欢使用 std::异常 而是考虑到 std::运行时错误 已经被滥用成一个有效的等级制度。如果这一做法在项目范围内得到广泛应用,它将有助于避免使用catch省略号- 捕获(…) 因为这最后还截取了非C++异常 Windows系统 (除非这正是你想要的)[更新:使用/EH或/EHsc选项而不是/EHa编译时,不会捕获Windows SEH异常。]

通过检查我的 C++ C++父书 ,对于自定义异常层次结构的根候选对象,他没有给出明确的建议,甚至在许多示例中使用普通对象。但也不是相反的建议。在关于异常的第14章中,他介绍了STL异常层次结构,将其作为STL组件抛出的异常家族,而不管您或项目中的其他开发人员可以构建的类可能是这个家族的扩展。但斯特劳斯特鲁普避免做的实际上是有影响力的推进小组通过 关于这件事的简短文章 . 在那里,他们认为 std::异常 为您自己的层次结构提供一个合理的基类,鼓励您认真考虑它。

相反,另一个MVP提到了这样一个事实:他主要在MFC应用程序中工作,并且在这种特定的上下文中,将您的异常建立在MFC中 C例外 因为MFC消息分派器有一个 尝试/抓住 这限制了其异常层次结构的范围。用他自己的话来说” 有一个学派认为,如果您已经设法让异常返回到MFC调度程序,那么您的代码已经是错误的。 “他申明,他对这一立场深表同情。

大多数MVP同意,尽管在C++中不违法,但是抛出原始类型 内景 , 长的 ,或类似的基于Windows的 HRESULT公司 等是一种编码恐惧,因为如果不能在适当的位置捕捉到它们,将导致应用程序崩溃,并需要进行一次艰难的事后调查,以确定它们的来源。一个更进一步,说 对我来说,这应该是终止雇佣关系的理由(至少是第二次这样做)。

然而,人们普遍认为,尽管异常层次结构有助于避免刚才提到的问题,但当异常消息的解释性不够(即 未找到记录 ,而不是类似于 未找到 ”)

防弹

作为旁注,有人批评MFC的一些设计是错误的 CFileException::GetErrorMessage() 方法使用基于指针的缓冲区作为参数,而不是 标准::字符串 作为当前C++学校推荐的(隐式)批评也达到了设计的目的。 std::异常 以及它的 什么() 方法返回一个旧的C样式 字符* ). 但是其他人提醒了一些智慧,因为异常处理机制被添加到C++中:异常操作本身不会抛出异常。从这个意义上说,标准字符串实现可以做到这一点,即它不能分配内存,最终导致 std::terminate() .

这听起来很合理,但原始思考者的反应也是原创的:如果您的应用程序处于这样的状态,即对于一个简单的异常消息没有剩余内存,那么导致无条件终止终究不会那么可怕。

没那么疯狂:在Java中,错误和异常是有区别的。后者是我们都知道的,并且在这里讨论的,但是错误属于一个被认为是致命的类别,在某种程度上,捕捉尝试是没有意义的。因此, java.lang.OutOfMemory 是错误,不是例外。

无论如何,这位MVP澄清说,他期望这种不正常的情况——不能创造一个 标准::字符串() 因为没有足够的内存-以低频率发生,尽管当第三个参与者问“即使在移动应用程序中?”

异常声明。与Java的比较

在辩论过程中,每当有人需要证明观点或提供例子时,更多的走廊就敞开着。因此,一个家伙敢于为动态异常声明(也就是说,一个函数或方法可能抛出的异常的规范)辩护,在我看来,错误地将Java异常声明模式作为一个模型来模仿。我出于几个或几个原因错误地说:

  • 在Java中,声明那些所谓的 检查型异常 与C++不同的目的是:强制声明的方法的调用方捕获或声明自己作为可能抛出的异常类型。如果不这样做,调用者方法将无法编译。 在ISO C++中,这个动态声明的目的是在运行时(而不是编译期间)确认任何抛出异常属于那些类型(或后代)。否则, std::意外() 虽然在VisualC++中,最后一个行为并没有按照标准指定。但是,C++中的异常声明不向调用方施加条件,这与上面提到的java情况不同。
  • 尽管是不同的物种,但今天两种语言都有一个广泛的信念,即声明例外是一种需要避免而不是建议的做法。在Java情况下,原因主要与可能出现的一些后续耦合有关。在C++中,如果声明异常的函数或方法调用的函数或方法引发新的异常,则可能会声明其异常被维护的痛苦。

我个人赞同当前的想法,但仍然对例外声明的辩护人所提供的理由感到有趣:在他的视野中,通过声明你可以控制什么地方可能失败,而不是让任何东西在任何地方失败。否则,缺少该控件将查找异常抛出位置的问题转化为指数问题。从这个意义上说,他更喜欢保持异常声明更新所带来的惩罚。

我不相信我会接受这个概念,尽管当您完全控制应用程序的整个代码库时(可能是这个MVP的情况),如果您依赖其他团队(即第三方)交付的组件,这种方法可能会有一个最佳点,在调用者和它调用的方法或函数之间保持这种程度的同步可能很快成为一项繁重的任务。

这实际上导致了我将在本文中介绍的最后一个方面,与异常处理相关。

异常处理

到目前为止,我们讨论的是抛出异常的时间。但一旦发生了,谁来管呢?幸运的是,它的直接调用方应该直接参与到捕获中的旧观念已经消失了,处理异常的正确观念在哪里和只有在哪里可以重新建立应用程序状态(否则就让它飞起来并保持展开状态)得到了广泛的传播。

还有一个共同的意识,即在一个n层应用程序(最终是n层)中,如果在给定的层上什么也不做,那么在离开它之前应该采取一些行动,作为重新建立应用程序状态的尝试。这个操作最终可能是部分的(比如记录诊断工具的原始故障),同时屏蔽异常或将异常转换为更有意义的内容,以便返回到直接的外层。在某些特殊情况下,屏蔽是不可能的,因为“外部世界”是一个不支持异常的层。有人举了一个例子,当我们要跨越COM方法的边界时。在这种情况下,一个可能的解决方案是将异常映射为某种形式的HRESULT。

有人提醒说,在资源级别,补偿不应该依赖于异常处理:只应用 雷伊 技术( 源获取即初始化 ,)只要本地资源失去可见性,补偿操作就必须在资源析构函数中发生。

异常生成和处理是书中涵盖章节的主题,因此尽管这篇文章(基于一个问题引发的随机讨论)提供了一些见解,但在这方面您可能还有很多其他建议。你愿意和我们分享这些吗?

附录:C++ 0x中的异常

作为讨论中的一个侧面注释,有人提到,动态异常规范将在即将到来的C++ 0x标准中被弃用。此外,一个 无例外 声明的意思与 抛出() 今天的声明,虽然不是完全等同(它是 范围仍在讨论中,所以我不想在这里写任何稍后必须更改的内容。)几个月前,有一篇关于它的有趣文章分三部分发表( , .)

琐事:你为什么这么想 尝试/抓住 C++中的块缺少 最后 ?

如果您熟悉Java和.NET,您可能已经想知道为什么没有 最后 C++中的块。你不知道为什么C++不需要它,不像托管语言吗?

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享