24 March 2022

C++ 核心指南目录

I.10: Use exceptions to signal a failure to perform a required task

理由

不可忽视那种会导致不可预期的系统状态或计算结果的错误。大部分系统错误是因为这种疏忽导致的。

例子

int printf(const char* ...);    // bad: return negative number if output fails

如果输出失败, printf 的返回值会是负数。如果检查返回值的话,这个故障就被我们忽视了。

template<class F, class ...Args>
// good: throw system_error if unable to start the new thread
explicit thread(F&& f, Args&&... args);

thread 如果构造失败,就会抛出异常:

  1. 构造函数被 explicit 修饰后, 就不能再被隐式调用了。
  2. thread 构造函数出错errc::resource_unavailable_try_again会抛出system_errorthread

注意

什么是错误?

错误,意味着某个功能无法实现预期的目的(包括无法达到后置条件)。代码调用时,忽略错误,可能导致错误的结果或不确定的系统状态。比如,无法与远程服务器建立连接本身可能不是一个错误:服务器也可能因为自身原因拒绝所有连接请求,所以最自然的事情是返回一个结果,让调用者去检查无法连接的原因。然而,如果认为无法建立连接是一种错误,那么应该抛出一个异常。

例外

传统的接口函数(如 Unix 信号处理函数)使用错误码( errno )汇报实际错误状态。没有其他替代方案,因此调用这类函数可以违反此规则。violate the rule.

替代方案

如果你不能使用异常(比如代码中使用很多老式的 raw pointer,或者有硬实时要求),考虑使用以下方式,返回一对值:

#include <tuple>
tuple<int, int> do_something() {
    return make_tuple(1, 2);
}
int main()
{
    int val;
    int error_code;
    tie(val, error_code) = do_something();
    if (error_code) {
        cout << "val= " << val << ", error= " << error_code << endl;
    }
    return 0;
}
val= 1, error= 2

这个方式会产生未初始化的变量。因此C++17的“结构化绑定”功能可以直接从返回值初始化变量:

struct S {
    int val;
    int error_code;
};
S do_something() {
    return S(1, 2);
}
int main()
{
    auto [val, error_code] = do_something();
    if (error_code) {
        cout << "val= " << val << ", error= " << error_code << endl;
    }
    return 0;
}
val= 1, error= 2

注意

  • 我们不认为性能是不用异常处理的原因。
  • 通常情况,显式的出错检查和处理消耗的时间和空间与异常处理差不多。
  • 通常,使用异常处理,简洁清晰的代码性能更好(通过程序优化,简化出错分支处理)。
  • 对于性能要求高的代码,可以将出错检查移到性能关键代码外。
  • 长期来看,普通的代码能更好的优化。声明性能指标前,最好仔细测量。

强化

  • 本理论指南,无法直接检查。
  • 审查使用 errno 的地方