C++编程规范:错误处理与异常
第68条:广泛地使用断言记录内部假设和不变式
- 断言一般只会在调试模式下生成代码,但是千万不要在assert语句中编写具有副作用的表达式。
- 要避免使用assert(false),应该使用assert(!”information message”)
- 不要使用断言报告运行时错误,而且不要用异常代替断言
第69条:建立合理的错误处理策略,并严格遵守
- 错误处理机制应该只在模块边界改变,每个模块都应该在其内部一致地使用一种错误处理策略和机制,并在接口中一致地使用一种。
- 如果模块内外所使用的策略不同,则所有模块入口函数都要直接负责由内到外的策略转换
- 按定义回调函数和线程主线函数是(或者可能是)位于模块边界,每个回调函数主体和线程主线函数主体都应该将内部错误机制转换为合适的接口错误策略。
第70条:区别错误与非错误
- 函数是最基本的工作单元,函数对其起始状态进行了假设(此假设被记录在前置条件中,调用代码负责满足而被调用代码负责验证),并执行了一个或多个操作(以其结果或后置条件的形式记录,作为被调用代码的函数负责满足后置条件) 函数应该承担维持一个或多个不变式的职责。
- 一个非私有的会改变状态的函数按其定义是其对象的一个工作单元,而且必须将对象从一个有效的维持不变的状态转变成另一个有效的维持不变的状态,在该函数体中,对象的不变式可能会被破坏,但是这没有什么关系,只要在成员函数的最后它们能够被重新建立就行了
- 错误就是阻止函数成功操作的任何失败,有三种类型:
- 违反或者无法满足前置条件
- 无法满足后置条件:如果函数有一个返回值的话,生成一个有效的返回值对象就是一个后置条件
- 无法重新建立不变式 任何其他情况都不是错误,所以不应该报告为错误
- 当且仅当有正当理由让所有调用代码在调用函数f之前检查和验证条件的有效性时,这个条件才应该是函数f的前置条件。
第71条:设计和编写错误安全代码
- 确保出线错误时程序应该处于有效状态,这就是所谓的基本保证
- 强保证:保证最终状态要么是最初状态,要么是所希望的目标状态
第72条:优先使用异常报告错误
- 应该使用异常而不是错误码来报告错误,但不能使用异常时,对于错误以及不是错误的情况,可以使用状态码
- 在C++中和通过错误码报告相比,通过异常报告错误有许多明显的优势:
- 异常不能不加修饰地忽略。 错误码在默认时是被忽略的,而即使是对错误码最低限度的处理,也需要显式地编写代码来接受错误并做出相应反应 异常则不能不加修改地忽略,要忽略异常,必须显式地捕获它
- 异常是自动传播的 错误码默认时不会跨越作用域传播,要让高层的调用函数知道低层的错误码,编写中间代码的程序员必须显式地编写代码传播错误。 而异常则是自动跨作用于传播的,直到得到处理为止。
- 有了异常处理,就不必在控制流的主线中加入错误处理和回复了。 错误码的检查代码和处理代码肯定会散布于控制流的主线中,这会使主控制流和错误处理代码更加难以理解和维护。 异常处理很自然地将错误检查和恢复都放进独立的catch代码块,
- 对于从构造函数和操作符报告错误来说,异常处理要优于其他方案。 复制构造函数和操作符的签名是预定义的,没有为返回码预留。 对于操作符来说,使用错误码虽然不能令人满意,但至少是可行的;它需要类似于errno的方法,或者采用一种更差的方法。而对于构造函数,使用错误码是不可行的,因为C++语言将构造函数异常和构造函数失败绑定在一起
- 错误码使用过度的一个表现是:应用程序需要不断地检查各种琐细的真条件或者不检查应该检查的错误码
异常使用过度的一个表现是:应用程序代码频繁地抛出和捕获异常,以致于try代码块成功与失败的次数是同一数量级的。
- 例外情况:以下情况下可以考虑使用错误码:
- 异常的优点不适用。已知直接的调用代码总是必须马上处理错误,因此绝不会或者几乎不会发生异常传播
- 抛出异常与使用错误码的实测性能差异比较明显。性能差异是实际测出的,这很可能是因为在循环的内部,而且需要经常抛出异常
- 在非常罕见的情况下,一些硬实时项目可能会遇到压力而考虑完全关闭异常处理,因为编译器异常处理机制的时间保证最差,一些关键操作很难或者不可能满足必须的时间要求。
第73条:通过值抛出,通过引用捕获
- 通过值抛出异常,通过引用捕获异常,这是与异常语义配合最佳的组合,当重新抛出相同的异常时,应该优先使用throw,避免使用throw e;
- 在抛出异常时,如果抛出指针,就需要处理内存管理问题,抛出指向栈分配的值的指针是不可行的,因为在指针到达调用处之前还没有展开,虽然抛出指向动态分配内存的指针时可行的,但是这样就将释放内存的负担放在了捕获处。 如果确实必须抛出指针,可以考虑抛出一个类似值的智能指针
- 除非抛出的是智能指针,否则通过引用捕获异常时唯一可行的好办法,通过值捕获普通值将在捕获处引起切片问题,这样会去除多态性,而通过引用捕获则能够保持异常对象的多态性
第74条:正确地报告、处理和转换错误
- 只要函数检查出一个它自己无法解决而且会使函数无法继续执行的错误,就应该报告错误。 需要具备的处理错误的知识包括在错误策略中定义的保证错误不跨越块边界和吸收析构函数中和和释放操作中出现的错误
- 如果没有足以对错误做有用处理的上下文,代码就不应该接受错误,如果函数自己不准备处理(或者转换,或者谨慎地吸收)错误,那么它应该允许或者使错误向上传播到能够处理它的调用代码。
- 例外情况:接受并且再次发送同样的错误以添加测试代码有时候是有用的,虽然错误实际上并没有得到处理。
第75条:避免使用异常规范
- 不要在函数中编写异常规范,除非不得以而为之
- 编写异常规范的几种情况:
- 非法的:在一个函数指针的typedef中
- 允许的:除没有typedef外
- 必需的:在某些虚函数的声明中,这些虚函数改写了具有异常规范的基类虚函数
- 隐式且自动的:编译器生成的构造函数、赋值操作符和析构函数的声明中
- 一种常见但是并不正确的看法是:异常规范能够静态地保证函数只抛出规范所列的异常,从而使编译器能够基于这种信息实施优化。
- 异常规范会使编译器在函数体周围以隐式地try/catch块的形式增加运行时开销,从而通过运行时检查强制函数确实只抛出所列的异常,除非编译器能够静态地证明绝对不能违反异常规范,只有在这种情况下它才可以自由地优化掉动态检查。
- 如果违反异常规范,默认情况下会马上终止程序,当然可以注册一个unexpected handler,但是因为只有一个全局处理函数,所以极有可能帮不上什么忙。
- 例外情况:如果不得不改写已经使用了异常规范的基类虚拟函数,而且又不能修改基类删去异常规范,那么只能在改写函数中写一个兼容的异常规范,同时应该尽量使其限制不少于基类版本,这样可以最大限度地减少违反异常规范的频率。