在C ++社区中是否存在关于何时应该使用异常的一般共识?

时间:2011-04-10 03:07:46

标签: c++ exception error-handling exception-handling

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

  1. 使用异常而不是错误代码
  2. 大多数情况下使用错误代码,仅在发生某些灾难性错误时才使用例外
  3. 这只是一个有争议的话题,没有被广泛接受的最佳实践吗?

11 个答案:

答案 0 :(得分:12)

正如你可能从丰富的答案中收集的那样,肯定没有达成共识。

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

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

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

另一方面:

  • 签名
  • 中完整记录了错误的可能性
  • 代码检查中的 silent 异常(阅读GotW #20: Code Complexity和cry)和隐藏的执行路径使推理变得更难。

这两个解决方案显得笨拙的原因很简单,错误检查很难。事实上,我每天写的大部分代码都涉及错误检查,无论是技术还是功能。

那该怎么办?

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

我个人喜欢在这里利用类型系统。典型的例子是指针引用二分法:指针就像一个可以为null的引用(并重新设置,但这里没关系)

因此,而不是:

// 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随之变化是正常的。

个人我倾向于仅对不可恢复的错误使用异常:因此我的代码中只有少量try / catch,只有在最外层才能准确记录错误(爱堆栈框架)和日志也转储了BOM。

这与Haskell非常相似(并且确实受到强烈影响),代码分为两个明确的部分:任何可以抛出异常,只有IO部分(extern部分)可能实际捕获它们。因此,纯部分必须以其他方式处理错误条件,以防它们“正常”。

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

答案 1 :(得分:10)

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

  1. 仅在异常情况下抛出异常。这听起来很明显,但是许多API构建时会抛出异常,大约50%的时间被调用(并且布尔返回状态更合适)。
  2. 在catch子句中,知道何时消耗异常,何时按原样重新抛出它们以及何时抛出不同类型的异常。我无法为你提供一个通用的规则,因为它非常依赖于你的应用程序的需求,但是如果你从这个方面拿走一件事,它应该是异常的默认消费可能是最糟糕的事情你的代码可以做到。每个框架都应该知道在出现问题时调用框架的预期。

答案 2 :(得分:8)

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

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

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

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

答案 3 :(得分:8)

不,但是没有达成共识。

编辑:正如您所看到的,从其他答案中可以看出,没有达成共识 - 只有思想流派,设计原则和行动计划。没有任何计划适合所有情况。

答案 4 :(得分:4)

即使达成共识,也不意味着它是有效的。您在此主要考虑的是实施和绩效成本。这些都是对项目和最终计划的真正限制。在这方面,有些事情需要考虑。

在运行时,传播的异常比简单的返回值更昂贵。这通常是在特殊情况下参数的来源。如果您在遭受性能损失时抛出了很多例外情况。 不要将此视为异常缓慢,它们仍然可以非常高效地实施,但仍然比错误代码更昂贵。

例外可以携带比错误代码更多的信息。类似于boost::exception库的东西允许标记信息,这些信息可以在异常链上提供大量有用信息。不像一个简单的错误代码说文件未找到,异常可以携带文件名,试图加载它的模块,当然还有底层错误代码。这种类型的信息很难通过堆栈中的错误代码传播。

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

异常catch语法很笨重。检查if语句中的返回代码通常要比编写catch块更容易。如果函数必须在不同的点捕获太多不同的异常,代码的含义将在大括号和try / catch子句中丢失。以下是一个概念的可能根源:如果函数调用通常会失败,则返回代码可能比异常更好。

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

了解团队的背景也可以提供帮助;考虑一下你是否有两个新的C ++程序员,一个来自C背景,另一个来自Java。有些人对错误代码感到满意,有些人则有例外情况。在每个方向上推动的距离都会影响您的项目并有助于提高整体质量。

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

答案 5 :(得分:3)

肯定没有达成共识。在高级别,我的口头禅是应该在“例外情况”中使用异常 - 也就是说,这些情况不是程序员错误的结果,而是执行环境中不可预测的情况。

围绕这个问题存在大量争论。根据您在编写程序时参与的其他实践 - 例如RAII - 异常可能或多或少地在惯用方面有用。

答案 6 :(得分:1)

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

答案 7 :(得分:1)

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

我在视频游戏行业工作的地方,在视频游戏系统上运行的代码中禁止例外。这是因为异常消耗CPU时间或内存,视频游戏系统都没有任何备用。

此外,在电子游戏中,很少有失败案例可以保证恢复正常;如果视频游戏失败,人类的生命或财产通常不会受到威胁。

答案 8 :(得分:1)

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

答案 9 :(得分:1)

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

  

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

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

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

答案 10 :(得分:0)

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

例如Iterator或Scanner上的hasNext()方法,在调用相应的下一个*()方法之前通常称为