引言

在程序设计的过程中,错误是常态,一个健壮的程序必须包含良好的错误处理。这要求我们首先理解错误的分类,并且掌握足够的工具。

一般地,错误可以分为可恢复的错误和不可恢复的错误:

  • 可恢复的错误是指那些对程序运行不致命的错误,例如用户传递了错误的参数

  • 相应地,不可恢复的错误是指对程序运行致命的错误,遇到这种错误,通常要清理资源后退出。

需要注意的是,上述两种分类所包含的具体错误类型并不是固定的,而是跟应用场景有关,例如,内存不足在某些情况下可能是一个可恢复的错误。

错误码

错误码也有多种形式:

  • 返回值专门用作错误码,例如 boolintstd::error_code 或自定义的类型

  • 返回值“无效”的部分用作错误码,例如 read 正常情况下应该返回一个正数,但是当遇到 EOF 或者出错时分别返回 0 和负数

异常

断言

断言用于检查内部逻辑必须满足的不变量(invariant),例如

// Private interface
//
// NOTE: Do NOT do this in real code, use loop instead.
static int fact_internal(int n, int acc) {
  // C++ idiom (because assert accepts only 1 argument)
  assert(n >= 0 && "n < 0");
  if (n < 2)
    return acc;
  return fact_internal(n-1, acc*n);
}

// Public interface
int fact(int n) {
  if (n < 0)
    throw std::invalid_argument();
  return fact_internal(n, 1);
}

实现一个简单的 assert

#ifndef NDEBUG
#  define cxxnotes_assert(expr) \
  do {                  \
    if (!(expr)) \
      _cxxnotes_assert_handler(__FILE__, __LINE__, #expr); \
  } while (false);
#else
#  define cxxnotes_assert(expr) (void)(expr)
#endif

[[noreturn]] inline void _cxxnotes_assert_handler(const char *file, int line, const char *expr_str) {
  fprintf(stderr, "at %s:%n: assertion `%s' violated", file, line, expr_str);
  std::abort();
}

异常的性能和 -fno-exceptions

尽管合法,但是异常的性能决定了它不适合作为控制流

默认的 libstdc++/libc++ 会使用异常,例如 new 如果分配的内存过大,会抛出 std::bad_alloc,使用 -fno-exceptions 只是禁用了异常处理,无法禁止 libstdc++ 抛出异常[bib:-fno-exceptions],如果你想,可以自己用 -fno-exceptions 编译一个无异常的标准库。但是,像 new 这种情况可能会直接 std::abort 而不是还能挽救。

回调

void get_all(const vector<URL> &urls,
             function<void(const Response &)> cb,
             function<void(const ErrorInfo &)> err_cb) {
    for (const auto &u : urls) {
        Response resp = http_client.get(u.str());
    }
}

实例:实现一个 Expected<T>

在一些函数式语言中,支持这样一种结构,用 C++ 表示大概是:

Result<int, ArithError> res = safe_add(left, right);
if (res)  // success
  std::cout << *res << '\n';
else
  std::cerr << res.get_error() << '\n';

这种特性底层依赖一种称为“代数数据类型”的机制(有时也会称为“tagged union”),大概是这样:

struct Result {
  enum Variant { OK, ERR } tag;
  union {
    T data;
    E err;
  } u;
};

但是,C++ 并没有天然支持这种结构,我们只能通过 structunion 的方式来模拟,然而,这样存在内存管理的问题,比较麻烦。C++17 引入了 std::variant 解决了大部分的问题,但是还存在一个小问题,就是各个类型必须是不同的,也就是说,这样的代码:

std::variant<int, int> v;

无法通过编译。

个人理解,这应该是因为 variant 支持 std::get 获取数据,如果两个唯独相同,就没办法确认 std::get<int> 到底想要获取哪一个;另外,这样还存在理解上的问题。

这个问题某种程度上导致我们不能直接依赖 std::variant 来实现 Expect<T>

那咋办呢?有句名言,所有的问题都能通过增加中间层解决,在这里,我们可以创建一个中间层包装 E

class ErrorInfo {
 public:
  virtual ~ErrorInfo() = default;
};

用户在使用时,需要先令表示错误的类继承自 ErrorInfo

class ArithError : public ErrorInfo {
 public:
  // enum classes cannot inherits other classes.
  enum class Type {
    OVERFLOW,
    DIV_BY_ZERO,
  };

 public:
  static const ArithError E_OVERFLOW{Type::OVERFLOW};
  static const ArithError E_DIV_BY_ZERO{Type::DIV_BY_ZERO};

 private:
  const Type typ_;
};

这样我们的接口就是 Expect<T> 而不是 Expect<T, E> 了。