在C++中使用异常的可能的错误处理策略是什么,它们的后果和影响是什么

What are the possible error handling strategies using Exceptions in C++, and what are their consequences and implications?

本文关键字:是什么 策略 影响 后果 处理 错误 C++ 异常      更新时间:2023-10-16

我希望您帮助了解在C++中使用/禁用异常的可能方法。

我的问题不是关于什么是最佳选择,而只是关于可能的选择是什么以及这些选择意味着什么。

目前,我能想到的选项是:

  1. 使用 -fno-exceptions 进行编译并放弃大多数 std 容器(可能定义不抛出的内部容器,如 SpiderMonkey Coding_Style 中建议的那样)
  2. 只是避免抛出和捕获自己的代码,但仍然使用可能会引发异常的 std 容器。令人高兴的是,在异常的情况下,程序可能会在没有堆栈展开的情况下终止,甚至 RAII 处理的外部资源也可能挂起。(根据这个SO问题的答案,这似乎是谷歌C++的方法)
  3. 不使用异常,但将所有内容包装在一个捕获所有 std::exception 尝试块中,以确保堆栈被展开,并且在程序终止之前释放外部资源的 RAII 句柄,至于这个证书C++规则
  4. 如上所述,但也抛出异常,最终将导致程序终止。
  5. 还使用捕获的异常并从异常中恢复。

我想知道我对选项的理解是否正确,以及我可能错过或理解错误的内容。

我还想知道授予基本异常安全的限制是否对选项 2-4(例外总是最终导致程序终止)有意义,或者是否可以/如何放宽/限制异常安全要求特定情况(例如处理外部资源、文件)。

更新

严格来说,当您想禁用异常时,仅编译而没有异常支持是真正的方法,因此选项 1。因为当您想禁用异常时,您也不应该使用甚至处理它们。引发异常会立即终止或进入大多数实现的硬故障,只有这样,您才能避免开销影响,例如空间或性能开销,即使它们有多小(见下文)。

但是,如果您只是想知道哪些关于异常使用的范式,那么您的选择几乎是正确的,您没有提到的一般想法是保持异常异常,并在程序启动时做可能甚至可能抛出的事情。

更一般地说,在一般的错误处理方面:异常用于处理您在运行时遇到的错误,如果正确完成错误,您只能在运行时检测到错误。您可以通过简单地确保在运行时之前解决所有可以检测到的问题来避免异常,例如正确的编码和审查(代码时间),使用严格的类型和检查(模板,编译时),然后再次重新检查它们(静态分析器)。

更新 2

如果你理解你对异常安全的关心是错误的,我会说: 基本上,首先它取决于何时启用异常:如果它们被禁用,您不能也不应该关心异常安全,因为没有异常安全(或者如果它们试图存在,您将终止/崩溃/硬故障无论如何)。 如果您启用了异常但不在代码中使用它们,例如情况 2、4 和 3,则无论如何您都不想终止,因此缺少清理代码无关紧要(并且 3. 中的内容在任何情况下仍然会运行)。但是,应该向所有人明确表示您不想使用它们,因此他们不会尝试从异常中恢复。 如果你在代码中使用它们,你应该关心异常安全的方式,当一个异常被抛出时,你也会清理你自己,然后是主处理程序或未来的代码更改,当你仍然终止或可以恢复时。不清理但仍使用异常是没有意义的。然后,您可以坚持使用选项1。

据我所知,这应该是详尽无遗的。有关更多信息,请参见下文。

建议

我推荐选项 4。 我告诉你为什么:

异常是一种非常误解和误用的模式。它们被滥用于程序流,就像Java语言过度使用一样。它们通常在嵌入式或安全代码中被禁止,因为它们的性质很难判断哪个处理程序会捕获它,如果有,以及这将需要多长时间,C++ std 基本上只是说"实现魔法"。

背景

然而,在我的推理中,对异常的仇恨基本上是一个很大的XY问题:当人们抱怨他们的恢复被破坏并且难以分辨时,通常的问题是,看不到你不能或应该对大多数例外做很多事情,这就是他们的目的。然而,像超时或关闭的 tcp 连接这样的事情几乎是不正常的,但许多人为此使用例外,这当然是错误的。但是,如果您的整个操作系统告诉您没有网络适配器或没有更多内存,该怎么办?您可能唯一想要的是尝试在某处记录原因,这应该在主块周围的一次尝试/捕获中完成。

安全/实时程序也是如此:对于内存不足的例子,当这种情况发生时,无论如何你都是****,那么策略是在不安全的初始化时间做这些事情,其中异常也没有问题。

当使用带有抛出成员的容器时,这是一个类似的场景,当你得到他们的错误代码时,你能做什么?不多,这就是为什么你在代码时确保没有错误的原因,比如确保一个元素确实在那里,或者为你的向量预留容量。

这有一个很好的好处,那就是代码更干净,不要忘记检查错误并且没有性能损失,至少使用 gcc 等常见C++实现。

3. 的原因是值得商榷的,在我看来你也可以这样做,但我的问题是:你必须在析构函数中做什么,无论如何都不会清理你的操作系统?操作系统将清理内存、套接字等。如果有一个独立的场景,问题仍然存在,无论何时不同:例如,您计划停止是因为您的UART破裂,您想在停止之前做什么?为什么你不能在重新投掷之前在捕获块中做到这一点?

总结一下

  1. 有不使用任何抛出代码的问题,并且仍然留下如何处理罕见错误代码的问题。(这就是为什么这么多C程序员仍然使用goto或跳远的原因)

  2. 恕我直言,不可行,两者中最糟糕

  3. 如前所述,好的,但是您需要在静态DTor中做什么,即使是非正常终止也不会做什么?

  4. 我的最爱

  5. 只有当你真的有罕见的疾病,你实际上能够从中恢复

我的意思是作为一般建议:引发和异常应该意味着发生了一些在正常操作中不应该发生的事情,但由于不可预见的错误而发生的,例如硬件故障、断开的电缆、未找到的共享库、您所做的编程错误,例如尝试在不在容器中的索引处 .at(),您可能已经知道, 但容器不能。那么几乎每次都抛出异常会导致程序终止/硬故障是合乎逻辑的

作为使用异常支持进行编译的结果,例如对于一个独立的ARM程序,你的程序大小将增加27kB,对于托管(Linux/Windows等)可能没有,对于独立情况可能没有2kB RAM(参见C++带有freerto的GNU arm cortex m4上的异常处理程序)

但是,当使用常见的编译器(如 clang 或 gcc)时,当您的代码正常运行时,即当您的分支/ifs 不会触发异常时,您不会因使用异常而支付性能损失。

作为参考,即证明我的陈述,我参考ISO/IEC TR 18015:2006 C++性能技术报告,其中摘录自7.2.2.3:

仅在以下情况下为实时关键程序启用异常处理 实际使用了例外。完整的分析必须始终包括 抛出异常,此分析将始终是 取决于实现。另一方面,采取行动的要求 在确定性时间内可能会在异常的情况下松动 (例如,当 连接已断开)。例外备选办法概述 处理在 §5.4 中给出。但如图所示,所有选项都有其 运行时成本和引发异常可能仍然是最好的方法 处理特殊情况。只要没有引发异常 路漫漫其修远兮(即 抛出表达式和处理程序),它甚至可能减少运行时 成本。