C++ 社区对何时应该使用异常有普遍的共识吗? [关闭]

Posted

技术标签:

【中文标题】C++ 社区对何时应该使用异常有普遍的共识吗? [关闭]【英文标题】:Is there a general consensus in the C++ community on when exceptions should be used? [closed] 【发布时间】:2011-08-02 08:14:33 【问题描述】:

我刚刚花了几个小时阅读关于何时使用异常的 SO 问题,似乎有两个观点不同的阵营:

    对错误代码使用异常 大部分时间使用错误代码,仅在发生某些灾难性错误时才例外

这只是一个没有被广泛接受的最佳实践的有争议的话题吗?

【问题讨论】:

如果您提到目标平台和设计空间,那将会有所帮助。在某些情况下(RAM 有限的微控制器或设备驱动程序等实时代码)答案是“从不”。 您可能有兴趣阅读Exceptions for Practically-Unrecoverable Conditions和Boost - Error and Exception Handling。 parashift.com/c++-faq-lite/exceptions.html 使阅读引人入胜! 如果异常在 C++ 生命的早期可用,那么现在可能会有更多共识 【参考方案1】:

没有达成共识,但正如回复所表明的那样,许多人认为仅在异常情况下才抛出异常。我不太喜欢这个建议,因为这样问题就变成了“什么是特殊情况?”。我更喜欢以下建议

当且仅当替代方案无法满足后置条件(包括所有不变量作为隐式后置条件)时,函数必须抛出异常。

是否在您的代码中写入throw 的实现决策随后与函数的后置条件的设计决策相关联。

参见 Herb Sutter 和 Andrei Alexandrescu 的 C++ 编码标准,第 70 条(区分错误和非错误)和第 72 条(更喜欢使用异常报告错误)。

【讨论】:

【参考方案2】:

我喜欢 Boost 的 Error and Exception Handling 指南。基本上,当需要展开堆栈时使用异常。这简单明了。

【讨论】:

你在讽刺吗?因为这对我来说似乎很模棱两可。 我不确定什么是模棱两可的。也许倒置的语句更清楚:除非您不想展开堆栈,否则请使用异常。【参考方案3】:

“C++ 社区”有派系,每个派系对异常都有自己的看法。

我在视频游戏行业工作的地方,在视频游戏系统上运行的代码中禁止出现异常。这是因为异常会消耗 CPU 时间或内存,而视频游戏系统都没有多余的资源。

此外,在电子游戏中,很少有需要优雅恢复的失败案例;如果电子游戏失败,人的生命或财产通常不会受到威胁。

【讨论】:

我认为我们需要一个不同的说明符来代表你所谓的“C++ 社区”派别,它不使用异常。没有例外的 C++ 并不是真正的 C++,而是完全不同的东西。我不想在这样的环境中工作(你描述了禁止异常的地方),就像我拒绝用汇编代码编写复杂的应用程序一样。【参考方案4】:

我没有在任何其他回复中看到这个问题,但是(如“Effective Java”一书中所述)在执行 OOP 时,跳过抛出异常的可能性是在对象中有一个辅助方法来“询问”是否要做的操作是可能的。

例如 Iterator 或 Scanner 上的 hasNext() 方法,通常在调用相应的 next*() 方法之前调用

【讨论】:

【参考方案5】:

不,但没有达成共识。

编辑:正如你所看到的,从其他答案中,没有达成共识——只有思想流派、设计原则和行动计划。没有任何计划能完美适用于所有情况。

【讨论】:

这不是真正的答案。 但这是正确的答案。有许多常见的概念,一些常见的做法,但我会毫不犹豫地说有任何最佳做法。许多人根本不使用异常。 如您所见,从其他答案中,没有达成共识——只有思想流派、设计原则和行动计划。没有计划完全适合所有情况。 (我将修改我的答案以包含此声明。) 我同意这里的观点,但发现自己不确定这是回答问题标题还是问题正文中的最后一个查询,并得出结论是后者。所以,正确的答案,但没有很好的回答。 @Steve,只有一个问题,并分析了与异常与错误代码主题相关的先前问题。不过,我可以处理反对票。 :)【参考方案6】:

正如您可能从大量答案中得出的结论,肯定没有共识。

在语义上,异常和错误提供完全相同的功能。实际上,它们在所有语义方面都是相同的,并且可以像异常一样任意丰富错误(您不必使用简单的代码,您可以使用真正的数据包!)。

唯一的区别是它们的传播方式:

错误必须手动传递 异常会自动传播

另一方面:

错误的可能性已完美记录在签名中 异常在代码检查中静默(阅读GotW #20: Code Complexity 并哭泣)并且隐藏执行路径使推理更加困难。

这两种解决方案都显得笨拙的原因仅仅是错误检查很困难。事实上,我每天编写的大部分代码都与错误检查有关,无论是技术上的还是功能上的。

那该怎么办?

警告:前面的演示,如果您只关心答案,请跳到下一部分

我个人喜欢在这里利用类型系统。典型的例子是指针引用二分法:指针就像一个引用,可以为空(并且可以重新定位,但在这里无关紧要)

因此,而不是:

// Exceptions specifications are better not used in C++
// Those here are just to indicate the presence of exceptions
Object const& Container::search(Key const& key) const throw(NotFound);

我会倾向于写:

Object const* Container::search(Key const& key) const;

或者更好的是,使用聪明的指针:

Pointer<Object const> Container::search(Key const& key) const;

template <typename O>
O* Pointer<O>::operator->() const throw(Null);

template <typename O>
O& Pointer<O>::operator*() const throw(Null);

在这里我发现使用异常是多余的,原因有两个:

如果我们正在搜索一个对象,那么没有找到它既是非常常见的情况,也没有太多数据可以携带:错误原因? 它不存在 客户不一定认为它不存在是一个错误,我有什么资格认为我比她更了解她的业务?我是谁来决定永远不会出现不找到所要求的东西不合适的情况?

我对异常本身没有问题,但它们会使代码变得笨拙,请考虑:

void noExceptions(Container const& c)

  Pointer<Object const> o = c.search("my-item");

  if (!o) 
    o = c.search("my-other-item");
  

  if (!o)  return;  // nothing to be done

  // do something with o

并将其与“异常”情况进行比较:

void exceptions(Container const& c)

  Object const* p = 0;
  try 
    p = &c.search("my-item");
  
  catch(NotFound const&) 
    try 
      p = &c.search("my-other-item");
    
    catch(NotFound const&) 
      return; // nothing to be done
    
  

  // do something with p

在这种情况下,使用异常似乎并不合适:/

另一方面:

try 
 print() << "My cute little baby " << baby.name() << " weighs " << baby.weight();

catch(Oupsie const&) 
  // deal

当然比:

if (!print("My cute little baby "))  /*deal*/ 
if (!print(baby.name()))  /*deal*/ 
if (!print(" weighs "))  /*deal*/ 
if (!print(baby.weight()))  /*deal*/ 

那么什么是最好的呢?

这取决于。像所有工程问题一样,没有灵丹妙药,一切都是让步。

所以请记住两件事:

错误报告是 API 的一部分 API 的设计应考虑到易用性

如果您发现自己想知道是否使用异常,只需尝试使用您的 API。如果没有明确的赢家,那就是:没有理想的解决方案。

哦,当发现在设计时选择的错误报告机制不再合适时,请毫不犹豫地重构您的 API。不要害臊:需求会随着时间而变化,所以 API 也会随之变化。

个人我倾向于只对不可恢复的错误使用异常:因此我的代码中很少尝试/捕获,仅在最外层,以准确记录错误(爱堆栈帧)并记录以及 BOM 的转储。

这与 Haskell 非常相似(并且确实受到了强烈影响),那里的代码被隔离在两个明确的部分中:虽然任何都可以抛出异常,但只有 IO 部分(外部部分)可以真正捕获它们。因此,纯部分必须以其他方式处理错误情况,以防它们“正常”。

但是,如果我遇到一个问题,即使用异常使代码更易于阅读且更自然(这是主观的),那么我使用异常:)

【讨论】:

很好的答案。我喜欢底线:使用使代码更简单的那个。受够了这种无意义的例外情况下的例外口头禅。 @Inverse:就像许多咒语一样,它们是为了给初学者一个好的经验法则:)【参考方案7】:

即使有共同的共识,也并不意味着它是有效的。您在这里的主要考虑应该是实施和性能成本。这些都是对项目和最终程序的真正限制。在这方面,这里有一些事情需要考虑。

在运行时,传播异常比简单的返回值更昂贵。这通常是 in exception cases 参数的来源。如果您一直在抛出大量异常,那么您将遭受性能损失。 不要认为这意味着异常很慢,它们仍然非常有效地实现,但仍然比错误代码更昂贵。

异常可以携带比错误代码更多的信息。 boost::exception 库之类的东西允许标记信息,这些信息可以在异常链上提供大量有用的信息。与简单的错误代码不同,找不到文件,异常可以携带文件名、尝试加载它的模块,当然还有底层错误代码。这种类型的信息很难通过错误代码向上传播。

错误代码传播在实施时可能很麻烦。任何不想处理错误的函数都必须将值向上传递。很多时候你会发现代码只是忽略了错误,因为程序员当时无法处理它,或者他不想重构他的函数签名来传递错误。

异常catch 语法庞大。在if 语句中检查返回码通常比编写catch 块要容易得多。如果一个函数必须在不同的点捕获太多不同的异常,那么代码的含义将在大括号和 try/catch 子句的海洋中丢失。这可能是这样一种观念的根源,即如果函数调用通常会失败,则返回代码可能比异常更好。

清楚地了解异常的工作原理将有助于您做出决定。有性能方面的考虑,但对于许多项目来说,这可以忽略不计。有关于异常安全的担忧,你必须知道在哪里使用异常是安全和不安全的。如果人们开始从函数中执行短期返回,错误代码也有同样的安全问题。

了解您的团队的背景也可以提供帮助;考虑一下您是否有两个新的 C++ 程序员,一个来自 C 背景,另一个来自 Java。有些人对错误代码更满意,而另一些人则对异常更满意。您在每个方向上的推进程度将影响您的项目并有助于提高整体质量。

最终没有明确的答案。虽然在某些情况下,一方可能会明显胜过另一方,但这在很大程度上取决于项目。查看您的代码并在适当的时候继续重构。有时您甚至无法选择使用什么。其他时候不会有什么不同。它是高度特定于项目的。

【讨论】:

关于代价:错误条件的代价是恒定的,实际上有没有错误都是一样的。在良好的实现中,异常仅在when 抛出时花费,否则是免费的(如在啤酒中)。对于不常见的问题,异常比错误代码更快。 @Matthieu,是的,不是的。设置 catch 语句的成本最低,但你是正确的,大部分成本只是在抛出某些东西时(假设你的编译器不糟糕,有些仍然如此)。是的,错误代码确实有固定的成本(正如我提到的那样很麻烦),但简单的错误返回在许多 CPU 上非常便宜。这里有很多细节,因此在每个项目的基础上考虑是非常重要的。 qa mort-ora-y:我知道异常的两种实现,最常用的(至少是 gcc/clang)是表驱动实现,它不需要设置捕获的成本(在运行时)但占用额外的内存(虽然不是在常规路径中)并且在实际抛出异常的情况下速度较慢。 你是对的。我仔细检查了 GCC,“零成本”异常确实是零成本。我写了一个小程序,它有两个函数,都调用另一个抛出异常(非内联但经过优化编译)。两个函数中的操作代码完全相同(GDB 反汇编命令)。带有 try/catch 的当然有异常处理尾声代码。【参考方案8】:

异常是正确从构造函数报告错误的唯一方式,因此选择很明确。抛出异常会迫使调用者捕获它并以某种方式处理它。带有返回错误代码的init() 方法的空构造函数的替代设计很容易出错,并且可能导致对象处于未知状态。

【讨论】:

不幸的是,我经常看到准系统构造函数与“初始化”方法一起返回成功或失败状态。 @chomp 同样,我也见过这样的设计。我已经编辑了我的答案。 我不同意。在使用对象池、第三方构造(或工厂)或序列化时,自定义对象管理通常需要使用init 函数来代替构造函数。因此,虽然构造函数的异常首选而不是init,但它们肯定不是唯一的方式。 我不喜欢因非致命原因而失败的构造函数。构造函数不应该做任何工作,或者尽可能少做。我所有的类都有一个微不足道的或空的构造函数体。在创建对象之后或之前进行初始化。 @rubenvb: 我不喜欢在我的代码中有 half-objects。 @Sam:另一种解决方案是使用构建器方法,这个方法可以失败而不必求助于异常。【参考方案9】:

肯定没有共识。在高层次上,我的口头禅是异常应该用在“异常情况”——即不是程序员错误而是执行环境中不可预知的情况导致的情况。

围绕这个问题有很多争论。根据您在编写程序时参与的其他实践(例如 RAII),异常可能或多或少具有惯用性。

【讨论】:

【参考方案10】:

我不认为这是 C++ 社区独有的讨论,但这里有两个对我有帮助的高级指南:

    仅在异常情况中抛出异常。这听起来很明显,但许多 API 在构建时都会在大约 50% 的调用时间引发异常(并且布尔返回状态会更合适)。 在您的 catch 子句中,了解何时使用异常、何时按原样重新抛出它们以及何时抛出不同类型的异常。我不能为此给你一个万能的规则,因为它非常依赖于你的应用程序的需求,但如果你从中拿走一件事,那应该是对异常的静默消费可能是最糟糕的事情你的代码可以做到。每个框架都应该了解调用框架在出现问题时的预期。

【讨论】:

我认为人们倾向于将异常情况理解为错误情况。应该注意的是,这不是必需的,只是意味着通常不会抛出异常,但有时会抛出异常。如果他们传达的不是错误情况,也可以。 @edA-qa 你能举个例子说明除了错误条件之外可以与异常进行通信的情况吗? 我实际使用的一个简单示例,在线程程序中,一个线程正在执行一些计算,主线程可能会使用它们。有时,一个线程会在计算完成之前请求计算,因此会使用异常返回到更高的处理程序。 我还在虚拟机和解释器中使用异常从代码中的较低点中断到外部处理程序。在这种情况下,中介是用户定义的,因此无法传播返回状态。【参考方案11】:

异常比错误代码更容易使用,因为它们可以被深度嵌套的子例程抛出,并且只能在可以处理的级别被拦截。

错误代码需要向上传递,因此调用另一个函数的每个函数都必须将错误代码传递回其调用者。

错误代码与异常相比没有任何明显的功能优势,因为异常可以在其中捆绑错误代码。有时错误代码可能比异常更有效,但我认为额外代码的成本和维护它的难度超过了任何可能的优势。

但是,错误代码存在于许多应用程序中,因为它们是用一种没有异常的语言编写或移植而来的,因此继续使用统一的错误处理方法是有意义的。

【讨论】:

谢谢。但是,如果错误代码对异常没有任何优势,那我为什么还要继续阅读应该只“在异常情况下使用异常”? @pepsi:错误码也表示异常情况,那有什么区别呢? @potatoswatter:我猜“例外”是一个相对术语。但是由于该引用通常被用作优先选择异常而不是错误代码的原因,因此我将其解释为“对比使用错误代码的条件更异常的条件使用异常”:) @pepsi:确实。同样,我尝试将 error 代码与单纯的 result 代码区分开来。如果一个函数专门执行了一个可能失败的操作,例如connect到一个套接字,失败应该由代码而不是异常来指示。稍后,如果套接字在读取时意外关闭,我希望这是一个异常,因为失败有更深层次的原因,与手头的任务无关,不太可能发生,与立即调用代码的关系也较小。 频率和错误传播应该是主要的决定因素。正如 Potatoswatter 所说,如果一个函数经常失败以至于调用者必须总是处理这种情况,那么错误代码可能是好的。而在写入情况下,通常唯一合理的处理程序位于调用图中较高的某个位置,因此传播返回代码既困难又可能丢失异常可能携带的信息。

以上是关于C++ 社区对何时应该使用异常有普遍的共识吗? [关闭]的主要内容,如果未能解决你的问题,请参考以下文章

我应该在 C++ 中使用异常说明符吗?

在 C++ 中通过指针捕获异常

何时可以/将在 C++ 中内联函数?可以强制内联行为吗?

GTK+ 接口应该在单独的线程中运行吗?

XCTAssertThrows 可以捕获 c++ 异常吗?

异常如何在 C++ 中工作(在幕后)