如何在C++中设计异常"类型"

voi*_*ter 30 c++ exception-handling

在我编写的大多数代码库中非常常见的两种反模式是布尔返回值以指示成功/失败,以及通用整数返回代码以指示有关错误消息的更多详细信息.

这两个都非常类似于C,并且在我的拙见中不能很好地适应C++.

我的问题是关于在代码库中设计异常时的最佳实践.换句话说,表明失败的有限可能性的最佳方法是什么?例如,上述反模式之一通常会有一个巨大的枚举,每个枚举值代表一种特定类型的失败,例如FILE_DOES_NOT_EXISTNO_PERMISSIONS.通常这些保持尽可能通用,以便它们可以跨多个不相关的域(例如网络组件和文件I/O组件)使用.

类似于此的设计可以考虑用于异常是std::exception为每种类型的故障或可能出错的事物子类化一个具体的异常类型.所以在我之前的例子中,我们将有以下内容:

namespace exceptions {
  class file_does_not_exist : public std::exception {};
  class no_permissions : public std::exception {};
}
Run Code Online (Sandbox Code Playgroud)

我认为这更接近"感觉更好"的东西,但最终这似乎是一个维护噩梦,特别是如果你有数百个这样的"错误代码"转换成类.

我看到的另一种方法是简单地使用标准<stdexcept>类,例如std::runtime_error并且具有包含细节的字符串.例如:

throw std::runtime_error( "file does not exist" );
throw std::runtime_error( "no permissions" );
Run Code Online (Sandbox Code Playgroud)

这种设计更易于维护,但如果它们都可能从相同的核心位置或函数调用中被抛出,则有条件地捕获这些异常中的任何一个是困难的或不可行的.

那么什么是异常类型的良好,可维护的设计?我的要求很简单.我想要了解发生了什么的上下文信息(我的内存耗尽了吗?我是否缺少文件系统权限?我是否未能满足函数调用的前提条件(例如,错误的参数)?),我也喜欢能够相应地对该信息采取行动.也许我对待所有这些都是一样的,也许我对某些失败有特定的捕获语句,所以我可以用不同的方式从它们中恢复.

我对此的研究只引出了这个问题: C++异常类设计

这里的用户问我一个类似的问题,他/她底部的代码示例几乎是可爱的,但他/她的基本异常类不遵循开放/封闭原则,所以这对我来说不起作用.

Che*_*Alf 30

C++标准库的异常层次结构是恕我直言,非常随意和毫无意义.例如,它可能只会产生问题,如果有人开始实际使用例如std::logic_error而不是终止时,很明显该程序有一个非常讨厌的Bug™.正如标准所说,

"逻辑错误的显着特征是它们是由程序内部逻辑中的错误引起的."

因此,在可能看起来合理的情况下抛出一个std::logic_error程序状态可能是不可预测的结果,并且继续执行可能会使用户的数据受到伤害.

尽管如此,std::string标准异常类层次结构仍然具有非常重要且实用的特性,即它是正式的标准.

因此,任何自定义异常类都应该间接派生或(尽管我不建议)直接派生std::exception.

一般来说,当关于自定义异常类的争论在十年前肆虐时,我建议仅从中获取std::runtime_error,我仍然建议这样做.它是支持自定义消息的标准异常类(其他通常具有硬编码消息,最好不应更改,因为它们具有可识别的价值).有人可能认为这std::runtime_error是表示可恢复故障的标准异常类(与不可恢复的逻辑错误相反,无法在运行时修复),或者正如标准所述,

"运行时错误是由超出程序范围的事件引起的.它们不能提前预测".

有时,C++异常机制用于其他事物,仅被视为低级动态目标跳转机制.例如,聪明的代码可以使用异常从递归调用链中传播成功的结果.但是异常失败是最常见的用法,这就是C++异常通常被优化的内容,因此大多数情况下使用std::runtime_errorroot作为任何自定义异常类层次结构是有意义的- 即使这会迫使想要聪明的人,抛出"失败" - 指示异常以表示成功......

值得注意的是:有三个标准的子类std::runtime_error,即std::range_error,std::overflow_errorstd::underflow_error,而相反的是他们的名字表示不需要后两者要由浮点运算生成,并且不是通过浮点运算产生的练习,但据我所知只有一些人产生 - 惊喜!- std::bitset运营.简单地说,标准库的异常类层次结构在我看来只是出于外观的考虑,没有任何真正的理由或现有的实践,甚至没有做出它有意义的检查.但也许我错过了那个,如果是的话,那么我仍然有一些新的东西可以了解这一点.:-)

那么,std::runtime_error就是这样.

在自定义异常类层次结构的顶部,使用C++ 03,添加C++ 03标准异常中缺少的重要内容非常有用:

  • clone方法(对于通过C代码传递异常尤其重要).

  • throwSelf方法(与克隆相同的主要原因).

  • 支持链式异常消息(标准化格式).

  • 支持携带故障导致代码(例如Windows或Posix错误代码).

  • 支持从承载的故障中获取标准消息导致代码.

C++ 11增加了对大部分内容的支持,但除了尝试新的支持失败导致代码和消息,并注意到它不是很特定于Unix并且不太适合Windows,我还没有使用它.无论如何,为了完整性:而不是添加克隆和虚拟重新抛出(这是普通应用程序程序员在自定义异常类层次结构中可以做的最好的,因为作为应用程序程序员,您不能将当前异常对象从实现的存储中提升出来异常传播用)时,C++ 11标准增加了自由函数std::current_exception()std::rethrow_exception()用于链接的异常消息支持,而是它增加了一个混合类std::nested_exception和自由功能std::rethrow_nestedstd::rethrow_if_nested.

鉴于对上述要点的部分C++ 11支持,新的和现代的自定义异常类层次结构应该更好地与C++ 11支持集成,而不是解决C++ 03的缺点.好吧,除了C++ 11失败代码之外,这似乎非常不适合Windows编程.因此,在自定义层次结构的顶部,在下面std::runtime_error,理想情况下,至少有一个通用异常类,并从中派生出一个支持传播失败代码的异常类.

现在,最后,问题的要点:现在是否应该最好为每个可能的失败原因派生一个独特的异常类,或者至少对于重大失败原因?

我说不:不要添加无需的复杂性.

如果或者它对于调用者来说可以用来区分某个失败原因,那么一个独特的异常类非常有用.但在大多数情况下,调用者感兴趣的唯一信息是发生异常的唯一事实.很少有不同的故障原因导致不同的尝试修复.

但是失败导致代码呢?

好吧,当这是底层API为您提供的内容时,只需添加工作来创建相应的异常类.但另一方面,当你在一个调用链中进行故障通信,并且调用者可能需要知道确切的原因时,那么使用代码就意味着调用者必须使用一些嵌套的检查和调度catch.所以这些是不同的情况:(A)您的代码是失败指示的原始来源,而(B)您的代码使用例如失败的Windows或Posix API函数,并且表示失败导致失败导致代码.