如何创建和执行例外合同?


33

我试图说服我的团队领导者允许在C ++中使用异常,而不是返回isSuccessful带有错误代码的布尔值或枚举。但是,我不能反驳他的批评。

考虑这个库:

class OpenFileException() : public std::runtime_error {
}

void B();
void C();

/** Does blah and blah. */
void B() {
    // The developer of B() either forgot to handle C()'s exception or
    // chooses not to handle it and let it go up the stack.
    C();
};

/** Does blah blah.
 *
 * @raise OpenFileException When we failed to open the file. */
void C() {
    throw new OpenFileException();
};
  1. 考虑开发人员调用该B()函数。他检查了它的文档,发现它没有返回异常,因此他没有尝试捕获任何东西。此代码可能会使程序在生产中崩溃。

  2. 考虑开发人员调用该C()函数。他不检查文档,因此不捕获任何异常。该调用不安全,可能会使程序在生产中崩溃。

但是,如果我们以这种方式检查错误:

void old_C(myenum &return_code);

如果使用该函数的开发人员不提供该参数,则会被编译器警告,并且他会说“啊哈,这将返回我必须检查的错误代码”。

如何安全使用异常,以便达成某种合同?


4
@Sjoerd的主要优点是,开发人员被编译器强制为函数提供变量以存储返回代码;他意识到可能存在错误,因此应该处理。这绝对比完全没有编译时间检查的异常更好。
DBedrenko

4
“他应该处理”-也没有检查这种情况是否发生。
2016年

4
@Sjoerd没必要。足以使开发人员意识到可能会发生错误,并且他们应该检查该错误。无论是否检查错误,评论者也都可以看到。
DBedrenko

5
您可能有更好的机会使用Either/ Resultmonad以类型安全的可组合方式返回错误
Daenyth 16/10/28

5
@gardenhead因为太多的开发人员没有正确使用它,所以最终以丑陋的代码结尾,并继续指责该功能而不是他们自己。Java中的检查异常功能是一件很漂亮的事情,但是由于某些人不了解,它的说唱不好。
AxiomaticNexus

Answers:


47

这是对例外的合理批评。 与简单的错误处理(例如返回代码)相比,它们通常不那么可见。而且没有简单的方法可以执行“合同”。这样做的部分目的是使您能够在更高级别上捕获异常(如果必须在每个级别上捕获每个异常,无论如何与返回错误代码有何不同?)。这意味着您的代码可能会被其他无法正确处理的代码调用。

例外确实有缺点。您必须根据成本效益提出理由。
我发现这两篇文章对您有所帮助:例外的必要性例外。 此外,此博客文章还提供了许多专家对异常的意见,重点是C ++。尽管专家的意见似乎倾向于例外,但远未达成明确的共识。

至于说服您的团队领导,这可能不是正确的选择。尤其是没有旧版代码。如以上第二个链接所述:

异常不能通过任何不安全的代码传播。因此,使用异常意味着项目中的所有代码都必须是异常安全的。

向主要不使用异常的项目中添加少量使用异常的代码可能不会有所改善。在编写良好的代码中不使用异常远不是灾难性的问题。可能根本不是问题,这取决于应用程序和您要求的专家。你必须选择自己的战斗。

这可能不是我要花力气的一个论点-至少要等到一个新项目开始时,我才开始努力。即使您有一个新项目,它也将被任何旧代码使用还是被其遗留代码使用?


1
感谢您的建议和链接。这是一个新项目,而不是旧项目。我想知道为什么当异常具有如此之大的缺点以致于使用不安全时,它们为什么如此普遍。如果您在更高级别上捕获到一个异常,然后在以后修改代码并四处移动,则无法进行静态检查以确保仍然捕获并处理了该异常(要让检查者深入检查所有功能,这太深了。 )。
DBedrenko

3
@Dee可以在上面的链接中找到支持例外的一些论点(我添加了带有更多专家意见的附加链接)。

6
这个答案是现货!
布朗

1
我可以根据经验说明,尝试将异常添加到预先存在的代码库中确实是一个坏主意。我已经使用过这样的代码库,因此使用起来真的非常非常困难,因为您永远都不知道会炸掉什么。仅在您事先计划了例外的项目中使用例外。
迈克尔·科恩

26

从字面上看,有整本书都写在这个主题上,所以任何答案都只能是总结。根据您的问题,以下是我认为值得提出的一些重要观点。这不是一个详尽的清单。


不应在所有地方都捕获异常。

只要在主循环中有一个通用的异常处理程序-根据应用程序的类型(Web服务器,本地服务,命令行实用程序...)-通常,您将拥有所需的所有异常处理程序。

在我的代码中,在主循环之外只有几个catch语句-如果有的话。这似乎是现代C ++中的常用方法。


异常和返回码不是互斥的。

您不应该将其作为一种全有或全无的方法。在特殊情况下应使用例外。诸如“找不到配置文件”,“磁盘已满”或其他无法在本地处理的内容。

常见的失败(例如检查用户提供的文件名是否有效)不是异常的用例;在这些情况下,请使用返回值。

从上面的示例中可以看到,根据使用情况,“找不到文件”可以是异常,也可以是返回码:“是安装的一部分”与“用户可以打错字”。

因此,没有绝对的规则。一个粗略的指导原则是:如果可以在本地处理,则使其成为返回值;如果您无法在本地处理它,则引发异常。


静态检查异常没有用。

由于无论如何都不应在本地处理异常,因此通常可以抛出哪些异常并不重要。唯一有用的信息是是否可以引发任何异常。

Java具有静态检查功能,但是它通常被认为是失败的实验,并且大多数语言(尤其是C#)由于没有这种类型的静态检查。这是有关C#没有它的原因的很好的阅读

由于这些原因,C ++已经过时了throw(exceptionA, exceptionB)赞成noexcept(true)。默认值是函数可以抛出的值,因此,除非文档中明确声明,否则程序员应该期望这样做。


编写异常安全代码与编写异常处理程序无关。

我想说的是,编写异常安全代码就是如何避免编写异常处理程序!

大多数最佳实践旨在减少异常处理程序的数量。一次编写代码并自动调用它(例如通过RAII)比在整个地方复制粘贴相同的代码所导致的错误更少。


3
只要有一个通用的异常处理程序...通常您就可以了。 ”这与大多数消息来源相矛盾,这强调编写异常安全代码非常具有挑战性。

12
@ dan1111“编写异常安全代码”与编写异常处理程序无关。编写异常安全代码是关于使用RAII封装资源,使用“首先在本地完成所有工作然后使用swap / move”以及其他最佳实践。如果您不习惯这些功能,那么这些功能将非常具有挑战性。但是“编写异常处理程序”不是这些最佳实践的一部分。
Sjoerd

4
@AxiomaticNexus在某种程度上,您可以将功能的每种不成功用法解释为“不良开发人员”。最好的主意是减少使用不当的主意,因为它们可以使正确的事情变得简单。静态检查的异常不属于该类别。它们所创造的工作与其价值不成比例,通常是在所编写的代码甚至根本不需要关心它们的地方。用户很容易以错误的方式使它们静音,以使编译器不再打扰他们。即使做得正确,它们也会导致很多混乱。
jpmc26

4
@AxiomaticNexus之所以不相称,是因为它可以轻松地传播到整个代码库中的几乎每个函数。当我一半的类中的所有函数都访问数据库时,并不是每个函数都需要显式声明它可以抛出SQLException;每个调用它们的控制器方法都不会。无论工作量多么小,这么多的噪声实际上都是负值。Sjoerd触手可及:您的大多数代码不需要关注异常,因为它无论如何也无法对其进行任何处理
jpmc26,2013年

3
@AxiomaticNexus是的,因为最好的开发人员是如此完美,他们的代码将始终完美地处理每一个可能的用户输入,并且您永远不会在产品中看到这些异常。如果这样做,那就意味着您是人生中的失败。说真的,不要给我这个垃圾。没有人能做到完美,甚至没有最好的。即使面对这些错误,您的应用也应该表现出理性,即使“理性”是“停止并返回一半的错误页面/消息”。猜猜是什么:这也与您对99%的IOExceptions 所做的相同。选中的划分是任意且无用的。
jpmc26

8

C ++程序员不寻找异常规范。他们寻找例外保证。

假设一段代码确实引发了异常。程序员可以做出哪些假设仍然有效?从编写代码的方式来看,代码在发生异常之后能保证什么?

还是某个代码段可以保证永远不会抛出该异常(即,除了OS进程被终止外)?

“回滚”一词在有关异常的讨论中经常出现。能够回滚到有效状态(已明确记录)的方法是异常保证的一个示例。如果没有例外保证,则程序应当场终止,因为甚至不能保证程序之后执行的任何代码都能按预期运行-例如,如果内存已损坏,则任何进一步的操作在技术上都是未定义的行为。

各种C ++编程技术可促进异常保证。RAII(基于范围的资源管理)提供了一种执行清除代码并确保在正常情况下和例外情况下都释放资源的机制。如果对对象执行修改,则在进行数据复制之前,如果操作失败,则可以恢复该对象的状态。等等。

这个StackOverflow问题的答案使人一窥C ++程序员去了解代码可能发生的所有可能的失败模式,并尽力保护程序状态的有效性,即使失败了。C ++代码的逐行分析成为一种习惯。

当使用C ++开发(用于生产用途)时,人们无法掩饰细节。同样,二进制Blob(非开源)是C ++程序员的祸根。如果我必须调用某个二进制Blob,但该Blob失败,则C ++程序员接下来将要做反向工程。

参考:http : //en.cppreference.com/w/cpp/language/exceptions#Exception_safety-请参阅“异常安全性”下的内容。

C ++在实现异常规范方面尝试失败。后来用其他语言进行的分析表明,异常规范根本不切实际。

为什么尝试失败:严格执行它,它必须是类型系统的一部分。但事实并非如此。编译器不检查异常规范。

为什么C ++选择了它,以及为什么其他语言(Java)的经验证明了异常规范尚无定论:当修改一个函数的实现时(例如,它需要调用另一个函数,这可能会引发一种新的异常)。例外),严格的例外规范执行意味着您还必须更新该规范。这会传播-您可能最终不得不为一个简单的更改而更新数十个或数百个功能的异常规范。对于抽象基类(相当于C ++的接口),情况会变得更糟。如果在接口上强制执行异常规范,则不允许接口的实现调用引发不同类型异常的函数。

参考:http : //www.gotw.ca/publications/mill22.htm

从C ++ 17开始,[[nodiscard]]可以在函数返回值上使用该属性(请参阅:https : //stackoverflow.com/questions/39327028/can-ac-function-be-declared-such-that-the-return-value -不能被忽略)。

因此,如果我进行代码更改,并且引入了一种新的失败条件(即,一种新的异常类型),这是否是一项重大更改?是应该迫使调用者更新代码,还是至少应警告更改?

如果您接受C ++程序员寻找异常保证而不是异常规范的论点,那么答案是,如果新的故障条件没有破坏代码先前承诺的任何异常保证,那不是一个重大改变。


“当修改一个函数的实现时(例如,它需要调用一个可能引发新异常的不同函数),严格的异常规范执行意味着您也必须更新该规范” –好,如您所愿。有什么选择?忽略新引入的异常?
AxiomaticNexus

@AxiomaticNexus也可以通过异常保证来回答。实际上,我认为规范永远不可能完整。保证是我们寻找的。通常,我们会比较异常类型,以尝试找出违反了什么保证的方式-以便猜测什么保证仍然有效。异常规范列出了您需要检查的一些类型,但是请记住,在任何实际系统中,列表都不可能完整。大多数时候,它迫使人们采取“吃掉”异常类型的捷径,将其包装在另一个异常中,这使得异常类型检查变得困难
rwong 16-10-28

@AxiomaticNexus我既不反对也不反对使用异常。典型的异常用法鼓励使用基本异常类和某些派生异常类。同时,必须记住,其他异常类型也是可能的,例如从C ++ STL或其他库抛出的异常类型。可以争论的是,在这种情况下可以使异常规范起作用:指定std::exception将抛出标准异常()和您自己的基本异常。但是,当将其应用于整个项目时,仅意味着每个功能都具有相同的规范:噪声。
rwong

我要指出的是,关于异常,C ++和Java / C#是不同的语言。首先,在允许任意代码执行或数据覆盖的意义上,C ++不是类型安全的;其次,C ++不会打印出良好的堆栈跟踪。这些差异迫使C ++程序员在错误和异常处理方面做出不同的选择。这也意味着某些项目可以合法地声称C ++不适合他们使用。(例如,我个人认为不允许使用C或C ++实现TIFF图像解码。)
rwong

1
@rwong:在遵循C ++模型的语言中,异常处理的一个大问题catch是基于异常对象的类型,而在许多情况下,重要的不是异常的直接原因,而是异常的含义系统状态。不幸的是,异常的类型通常不会说明是否导致代码破坏性地退出一段代码,从而使对象处于部分更新(从而无效)的状态。
supercat

4

考虑开发人员调用C()函数。他不检查文档,因此不捕获任何异常。通话不安全

完全安全。他不需要在每个地方都捕获异常,他可以在一个实际可以做一些有用的事情的地方抛出一个try / catch。如果他允许它泄漏出线程,这是不安全的,但是通常很难防止这种情况的发生。


这是不安全的,因为他不希望从中产生异常C(),因此不能避免跳过调用后的代码。如果需要该代码来保证某些不变性仍然成立,则该保证在出现的第一个异常时就显得毫无意义C()。没有完美的异常安全软件,最肯定的是从来没有出现异常的地方。
cmaster

1
@cmaster 他不希望出现异常C()是一个大错误。C ++中的默认值是任何函数都可以抛出-它需要显式noexcept(true)告诉编译器,否则。在没有这个承诺的情况下,程序员必须假定函数可以抛出。
舍尔德

1
@cmaster这是他自己的愚蠢错误,与C()的作者无关。异常安全是一回事,他应该了解并遵循。如果他调用一个他不知道也不例外的函数,那么该死的他应该很好地处理如果抛出的异常。如果他没有其他需要,他应该找到一个实现。
DeadMG '16

@Sjoerd和DeadMG:尽管标准允许任何函数抛出异常,但这并不意味着项目必须在其代码中允许异常。如果您禁止代码中的异常,就像OP的团队负责人似乎在做的那样,则不必进行异常安全的编程。而且,如果您试图说服团队负责人允许将异常放入以前没有异常的代码库中,则会有大量C()功能在出现异常时中断。这确实是非常不明智的事情,注定会导致大量问题。
cmaster

@cmaster这是一个新项目-请参阅已接受答案的第一条评论。因此,没有现有功能会中断,因为没有现有功能。
Sjoerd

3

如果要构建关键系统,请考虑遵循团队负责人的建议,不要使用异常。这是洛克希德·马丁公司联合打击战斗机C ++编码标准中的 AV规则208 。另一方面,MISRA C ++准则具有非常特殊的规则,涉及在构建符合MISRA的软件系统时何时可以使用例外以及何时不能使用例外。

如果要构建关键系统,则可能还会运行静态分析工具。如果您不检查方法的返回值,许多静态分析工具都会发出警告,从而使遗漏错误处理的情况显而易见。据我所知,用于检测正确的异常处理的类似工具支持并不那么强大。

归根结底,我认为通过合同和防御性编程进行设计以及静态分析对于关键软件系统来说比异常情况更安全。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.