【发布时间】:2010-10-03 15:16:39
【问题描述】:
【问题讨论】:
-
通常应该使用返回值来向函数调用者返回一个值,并使用异常来表示异常情况。如果不了解您的情况的更多细节,就不可能给出具体的建议。
-
我的问题不是针对特定情况的。我觉得异常和错误代码(返回值)都满足相同的需求。所以,我正在寻找可以帮助我决定选择哪一个的指南。谢谢。
【问题讨论】:
错误通常在代码中非常低的级别检测到,但在非常高的级别进行处理。如果您对这些使用返回代码,则必须设计所有中间级别以期望低级别代码并将其传播直到有东西处理它。除了例外,您只需要在低级别抛出,并在高级别捕获(如果有的话)。只要您的中级代码使用 RAII,您就不必更改任何内容来进行错误传播。
所以我经常考虑错误的类型以及可能在哪里处理它。如果调用者很可能会处理问题,因为它被认为是常见的事情,那么返回码就很好。如果错误是灾难性的(无法分配必要的资源),则不能指望直接调用者处理它。
其他需要考虑的事情:异常不能被忽略,返回码可以。如果问题得不到处理会很危险,请使用异常。
从技术上讲,异常不能通过 C 代码传播回来。 (它适用于许多系统,但它不是可移植的,也不能保证。)所以如果你有一个 C 库回调到你的 C++ 代码中,你可能不应该从回调中抛出异常。线程边界等也是如此。
【讨论】:
其中一些内容可能会重复,但以下是使用其中一个或另一个的简单提示:
您的函数可能无法使用标记值来发出错误信号,因为函数会将所有可能的值用作有效答案。这可能发生在不同的情况下,最值得注意的是整数数值算法。 0 和 -1 通常用作特殊值,但一些相当常见的算法,包括 pow 可能无法使用它们(即 0^1=0 和 -1^3=-1)。因此,选择合适的值成为一种艺术,一致性是一个问题,用户必须记住每个特殊情况的错误值。
一些 API 认识到这一点,并且(几乎)从不使用真正的返回值,而是始终依赖按引用返回的语义,并将所有函数中的返回值用作状态代码,或者是某个(标准)“成功”值,或特定于函数的错误代码。例如,CUDA 就有这样的约定。
错误代码通常被称为“更有效”(其他答案之一中的评论解释了为什么这不一定是正确的)。但是,它们存在两个常见问题。
The operation failed. 消息而不是 Ran out of disk space.。为了解决 (1),默认情况下会传播异常。故意忽略错误在代码中变得很明显,而不是隐藏。为了解决(2),异常使用类型系统,防止您编译具有冲突错误“值”的程序。此外,使用异常类层次结构,您可以表示相关结果的“家族”。 (注意:这经常被滥用,人们捕捉到Exception 而不是NoMoreDiskSpace 并且仍然显示通用的The operation failed. 消息)。
有些人会建议在他们的应用程序中混合使用这两种方法,但恕我直言,这会导致两种系统都被误用的情况。在这些混合约定中,由于混淆,通常不会捕获异常并且不会检查错误代码。一方面,由于异常仅用于异常情况,因此假定它们将永远发生,或者根本无法处理。另一方面,返回错误代码的故障被认为是次要的,根本不处理。当然,由每个程序员来决定情况是否是异常,这会导致在检查哪些错误代码和要捕获哪些异常之间产生很多混淆。
无论您选择哪种系统,请务必充分利用它,并始终如一地使用它。
【讨论】:
您可以将异常和返回值视为调用者和被调用者之间的两种不同通信方法。返回值是通知父母事情的好而快速的方式,但粗鲁的表亲是例外,你可以解雇它并让其他人知道“那里”有多糟糕。
如果您编写的代码尝试处理每种情况,您将使用返回值、错误代码等填充它。如果您忽略处理某些情况,这些情况发生在调用堆栈的日志中,并希望快速解决此问题 -此时有人会显示 MessageBox - 但您可以在上面的任何适当级别抛出并处理它。 也许这就是您问题的答案!
再想一想,以下是一些可能适用的规则:
我想这两种方法都将继续存在。随着时间的推移,你的判断会决定在每种情况下使用哪个。
【讨论】:
抛出异常时会对性能产生影响,这就是错误代码用于更频繁的错误条件的原因。为真正不寻常的问题保存异常。
例如 - 在 STL 中,如果没有找到匹配项,std::find 不会抛出。但是如果内存耗尽,vector::push-back 会抛出。
【讨论】:
find 也没有使用错误代码,而且找不到对象也可以说不是错误,因为这种可能性在其合同中得到了很好的说明。
find 有一个返回值,如果没有匹配,它将是(缩写).end()。这就是我的意思。它不使用异常来表示不匹配,而是使用函数的返回值。
find 认为找不到对象是正常路径,而不是错误。 STL 算法中的结束迭代器经过专门设计,以便算法可以使用 end 作为标记值,作为替代但非错误情况的信号。
C++ 异常应该只用于异常错误。
当错误严重到调用函数无法或确实不应该处理它时,它们特别有用,例如内存耗尽。抛出的异常会展开堆栈,直到它看到的第一个 try 块(或者可能是第一个带有适当 catch 的 try 块,这会更有意义)。这允许您从深度嵌套的函数调用中捕获异常错误,并确保堆栈被清理到带有 try 块的函数。
不过,这一切都会带来开销,因此当您可以处理调用例程中的(非异常)错误时,请使用返回码。
【讨论】:
除了 bjskishore123 在他的评论中与问题相关的问题中已经提出的讨论之外,Ned Batchelder 和 Joel Spolsky 在这个主题上还有一个非常有趣的历史来回。您可能并不完全同意 Ned,但他的论点经过深思熟虑,值得一读。
【讨论】:
如果您的函数没有返回任何内容,那么生活很简单——它可以返回成功/失败代码。但是当您的函数已经对其返回值有意义时,您需要使用“信号值”来返回错误代码。例如,查找返回 -1 的方法正在使用信号值。有时几乎不可能得出好的信号值。如果一个函数返回下一个可用日期,那么当没有可用日期时它应该返回什么?真的没有“-1”类型的日期。所以这是异常是一种非常有用的机制的一种情况。 (这也处理像构造函数这样不返回任何内容的函数。)
是的,抛出和捕获异常比检查错误代码稍微贵一些。但它不能被遗忘,许多人发现它更具表现力。而且,正如其他答案中所提到的,当存在级联函数调用时,让所有中间调用检查返回值并将其传递回链上是很尴尬的。
【讨论】:
如果出现问题并不罕见,请使用错误代码。例如,打开文件或建立网络连接的代码不应返回异常——出现问题是很常见的。
当出现异常情况时使用异常 - 失败是真正异常的。
所以,可以简化为:
【讨论】: