C++ Error Handling

C 标准库

当我们调用 C 函数时,一般会返回一个错误代码,如果想知道错误代码的含义,库一般也会提供一个检索错误字符串的函数 strerror 来获取错误描述。

这种方式麻烦的是,当我们基于其它库开发新的库时,库的错误代码与基础库的错误代码产生冲突,需要处理这种情况。

void perror_demo() {
  if (FILE* f = fopen("unexist.ent", "rb")) {
    fclose(f);
    return;
  }
  std::cerr << errno << ": " << strerror(errno) << std::endl;
}

// 2: No such file or directory

C++ 异常机制

到了 C++ 时代,其语言提供了异常机制 try...catch 让调用者可以捕获异常,但是我们只能通过异常对象类型和 what() 获取错误描述,没有了错误代码。

在 C++ 中难免需要调用遗留的 C 代码库,这时难以将错误代码传递给调用者,只能定义一个异常类传递错误字符串。或者,在自定义的异常类中存储错误代码。

有很多 C++ 代码库并不使用异常,仍使用 C 标准库类似的方式返回错误代码,反而是在调用 C++ 标准库抛出异常时,将其映射到一个自定义的错误代码。

void except_demo() {
  std::ifstream f;
  f.exceptions(std::ifstream::failbit | std::ifstream::badbit);
  try {
    f.open("unexist.ent", std::ifstream::in | std::ifstream::binary);
    f.close();
  } catch (const std::exception& e) {
    std::cerr << e.what() << std::endl;
  }
}

// ios_base::clear: unspecified iostream_category error

C++11 错误代码

自从 C++11 开始,C++ 标准库引入了 boost.system_error,在这个库里可以将错误分类 error_category,用户可以派生它实现自己的错误类别,在这个类里存储了错误类别的名字,以及错误代码和错误描述的映射。然后,调用者可以通过 error_code 访问其中的信息,也可以用异常机制来捕获 system_error

用户可以通过错误类别区分是哪种类型或哪个模块产生的错误,只是需要库编写 error_category 来映射错误。

使用 system_error 一般有两种形式,一种是通过类似 C 标准库的方式在函数原型中增加一个错误代码的参数来存储错误,一种是使用 C++ 的异常机制来捕获异常。

void return_error_code(std::error_code& ec) {
  ec = std::make_error_code(std::errc::no_such_file_or_directory);
}

void error_code_demo() {
  std::error_code ec;
  return_error_code(ec);
  if (ec) {
    std::cerr << ec.value() << ": " << ec.message() << std::endl;
  }
}

// 2: No such file or directory

void throw_system_error() {
  throw std::system_error(
      std::make_error_code(std::errc::no_such_file_or_directory));
}

void system_error_demo() {
  try {
    throw_system_error();
  } catch (const std::system_error& e) {
    std::cerr << e.code().value() << ": " << e.what() << std::endl;
  }
}

// 2: No such file or directory

LEAF 轻量级错误处理增强框架

Boost 1.75 开始引入了 LEAF,我个人认为这个库牛逼的地方在于它利用了模板实现例化的特性,当你不需要获取具体错误时,你能够判断是否产生了错误,但它不会实例化错误对象。

LEAF 能够兼容处理各种错误,但你需要在 try_handle_some/all 等中定义一系列错误处理函数,这意味兼容性强了,但处理错误的地方变复杂了,因为你需要知道库里面是有哪些错误类型,不像 errornoerror_code 能够很简单一致性的访问错误。

boost::leaf::result<void> return_leaf_result() {
  return boost::leaf::new_error(
      std::make_error_code(std::errc::no_such_file_or_directory));
}

void leaf_result_demo() {
  [[maybe_unused]] auto r = boost::leaf::try_handle_some(
      return_leaf_result, [](const std::error_code& ec) {
        std::cerr << ec.value() << ": " << ec.message() << std::endl;
      });
}

// 2: No such file or directory

void throw_system_error() {
  throw std::system_error(
      std::make_error_code(std::errc::no_such_file_or_directory));
}

void leaf_except_demo() {
  boost::leaf::try_catch(throw_system_error, [](const std::system_error& e) {
    std::cerr << e.code().value() << ": " << e.what() << std::endl;
  });
}

// 2: No such file or directory

线程间的错误传递

我们知道,当我们在子线程中抛出异常时需要在子线程中处理,否则将导致程序崩溃。而当我们使用线程池时,往往希望在父线程中统一处理子线程的异常,C++11 引入 exception_ptr 使得我们可以将子线程中的异常转移出来统一处理。

void worker() {
  try {
    std::this_thread::sleep_for(std::chrono::seconds(1));
    throw std::runtime_error("To be passed between threads");
  } catch (...) {
    teptr = std::current_exception();
  }
}

int main() {
  std::thread thrd(worker);
  thrd.join();

  if (teptr) {
    try {
      std::rethrow_exception(teptr);
    } catch (const std::exception& ex) {
      std::cerr << "Thread exited with exception: " << ex.what() << "\n";
    }
  }
}