<audio id="audio" title="06 | 异常：用还是不用，这是个问题" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/ab/4f/abea4ae1ab39cce76ac8e7aad4a1864f.mp3"></audio>

你好，我是吴咏炜。

到现在为止，我们已经有好多次都提到异常了。今天，我们就来彻底地聊一聊异常。

首先，开宗明义，如果你不知道到底该不该用异常的话，那答案就是该用。如果你需要避免使用异常，原因必须是你有明确的需要避免使用异常的理由。

下面我们就开始说说异常。

## 没有异常的世界

我们先来看看没有异常的世界是什么样子的。最典型的情况就是 C 了。

假设我们要做一些矩阵的操作，定义了下面这个矩阵的数据结构：

```
typedef struct {
  float* data;
  size_t nrows;
  size_t ncols;
} matrix;

```

我们至少需要有初始化和清理的代码：

```
enum matrix_err_code {
  MATRIX_SUCCESS,
  MATRIX_ERR_MEMORY_INSUFFICIENT,
  …
};

int matrix_alloc(matrix* ptr,
                 size_t nrows,
                 size_t ncols)
{
  size_t size =
    nrows * ncols * sizeof(float);
  float* data = malloc(size);
  if (data == NULL) {
    return MATRIX_ERR_MEMORY_INSUFFICIENT;
  }
  ptr-&gt;data = data;
  ptr-&gt;nrows = nrows;
  ptr-&gt;ncols = ncols;
}

void matrix_dealloc(matrix* ptr)
{
  if (ptr-&gt;data == NULL) {
    return;
  }
  free(ptr-&gt;data);
  ptr-&gt;data = NULL;
  ptr-&gt;nrows = 0;
  ptr-&gt;ncols = 0;
}

```

然后，我们做一下矩阵乘法吧。函数定义大概会是这个样子：

```
int matrix_multiply(matrix* result,
                    const matrix* lhs,
                    const matrix* rhs)
{
  int errcode;
  if (lhs-&gt;ncols != rhs-&gt;nrows) {
    return MATRIX_ERR_MISMATCHED_MATRIX_SIZE;
    // 呃，得把这个错误码添到 enum matrix_err_code 里
  }
  errcode = matrix_alloc(
    result, lhs-&gt;nrows, rhs-&gt;ncols);
  if (errcode != MATRIX_SUCCESS) {
    return errcode;
  }
  // 进行矩阵乘法运算
  return MATRIX_SUCCESS;
}

```

调用代码则大概是这个样子：

```
  matrix c;

  // 不清零的话，错误处理和资源清理会更复杂
  memset(&amp;c, 0, sizeof(matrix));

  errcode = matrix_multiply(c, a, b);
  if (errcode != MATRIX_SUCCESS) {
    goto error_exit;
  }
  // 使用乘法的结果做其他处理

error_exit:
  matrix_dealloc(&amp;c);
  return errcode;

```

可以看到，我们有大量需要判断错误的代码，零散分布在代码各处。

可这是 C 啊。我们用 C++、不用异常可以吗？

当然可以，但你会发现结果好不了多少。毕竟，C++ 的构造函数是不能返回错误码的，所以你根本不能用构造函数来做可能出错的事情。你不得不定义一个只能清零的构造函数，再使用一个 `init` 函数来做真正的构造操作。C++ 虽然支持运算符重载，可你也不能使用，因为你没法返回一个新矩阵……

我上面还只展示了单层的函数调用。事实上，如果出错位置离处理错误的位置相差很远的话，每一层的函数调用里都得有判断错误码的代码，这就既对写代码的人提出了严格要求，也对读代码的人造成了视觉上的干扰……

## 使用异常

如果使用异常的话，我们就可以在构造函数里做真正的初始化工作了。假设我们的矩阵类有下列的数据成员：

```
class matrix {
  …
private:
  float* data_;
  size_t nrows_;
  size_t ncols_;
}

```

构造函数我们可以这样写：

```
matrix::matrix(size_t nrows,
               size_t ncols)
{
  data_  = new float[nrows * ncols];
  nrows_ = nrows;
  ncols_ = ncols;
}

```

析构非常简单：

```
matrix::~matrix()
{
  delete[] data_;
}

```

乘法函数可以这样写：

```
class matrix {
  …
  friend matrix
  operator*(const matrix&amp;,
            const matrix&amp;);
};

matrix operator*(const matrix&amp; lhs,
                 const matrix&amp; rhs)
{
  if (lhs.ncols != rhs.nrows) {
    throw std::runtime_error(
      "matrix sizes mismatch");
  }
  matrix result(lhs.nrows, rhs.ncols);
  // 进行矩阵乘法运算
  return result;
}

```

使用乘法的代码则更是简单：

```
matrix c = a * b;

```

你可能已经非常疑惑了：错误处理在哪儿呢？只有一个 `throw`，跟前面的 C 代码能等价吗？

异常处理并不意味着需要写显式的 `try` 和 `catch`。**异常安全的代码，可以没有任何 `try` 和 `catch`。**

如果你不确定什么是“异常安全”，我们先来温习一下概念：异常安全是指当异常发生时，既不会发生资源泄漏，系统也不会处于一个不一致的状态。

我们看看可能会出现错误/异常的地方：

<li>
首先是内存分配。如果 `new` 出错，按照 C++ 的规则，一般会得到异常 `bad_alloc`，对象的构造也就失败了。这种情况下，在 `catch` 捕捉到这个异常之前，所有的栈上对象会全部被析构，资源全部被自动清理。
</li>
<li>
如果是矩阵的长宽不合适不能做乘法呢？我们同样会得到一个异常，这样，在使用乘法的地方，对象 `c` 根本不会被构造出来。
</li>
<li>
如果在乘法函数里内存分配失败呢？一样，`result` 对象根本没有构造出来，也就没有 `c` 对象了。还是一切正常。
</li>
<li>
如果 `a`、`b` 是本地变量，然后乘法失败了呢？析构函数会自动释放其空间，我们同样不会有任何资源泄漏。
</li>

总而言之，只要我们适当地组织好代码、利用好 RAII，实现矩阵的代码和使用矩阵的代码都可以更短、更清晰。我们可以统一在外层某个地方处理异常——通常会记日志、或在界面上向用户报告错误了。

## 避免异常的风格指南？

但大名鼎鼎的 Google 的 C++ 风格指南不是说要避免异常吗 [1]？这又是怎么回事呢？

答案实际已经在 Google 的文档里了：

> 
Given that Google’s existing code is not exception-tolerant, the costs of using exceptions are somewhat greater than the costs in a new project. The conversion process would be slow and error-prone. We don’t believe that the available alternatives to exceptions, such as error codes and assertions, introduce a significant burden.
Our advice against using exceptions is not predicated on philosophical or moral grounds, but practical ones. Because we’d like to use our open-source projects at Google and it’s difficult to do so if those projects use exceptions, we need to advise against exceptions in Google open-source projects as well. Things would probably be different if we had to do it all over again from scratch.


我来翻译一下（我的加重）：

> 
鉴于 Google 的现有代码不能承受异常，**使用异常的代价要比在全新的项目中使用异常大一些**。转换[代码来使用异常的]过程会缓慢而容易出错。我们不认为可代替异常的方法，如错误码或断言，会带来明显的负担。
我们反对异常的建议并非出于哲学或道德的立场，而是出于实际考虑。因为我们希望在 Google 使用我们的开源项目，而如果这些项目使用异常的话就会对我们的使用带来困难，我们也需要反对在 Google 的开源项目中使用异常。**如果我们从头再来一次的话，事情可能就会不一样了。**


这个如果还比较官方、委婉的话，Reddit 上还能找到一个更个人化的表述 [2]：

> 
I use [**sic**] to work at Google, and Craig Silverstein, who wrote the first draft of the style guideline, said that he regretted the ban on exceptions, but he had no choice; when he wrote it, it wasn’t only that the compiler they had at the time did a very bad job on exceptions, but that they already had a huge volume of non-exception-safe code.


我的翻译（同样，我的加重）：

> 
我过去在 Google 工作，写了风格指南初稿的 Craig Silverstein 说过**他对禁用异常感到遗憾**，但他当时别无选择。在他写风格指南的时候，不仅**他们使用的编译器在异常上工作得很糟糕**，而且**他们已经有了一大堆异常不安全的代码了**。


当然，除了历史原因以外，也有出于性能等其他原因禁用异常的。美国国防部的联合攻击战斗机（JSF）项目的 C++ 编码规范就禁用异常，因为工具链不能保证抛出异常时的实时性能。不过在那种项目里，被禁用的 C++ 特性就多了，比如动态内存分配都不能使用。

一些游戏项目为了追求高性能，也禁用异常。这个实际上也有一定的历史原因，因为今天的主流 C++ 编译器，在异常关闭和开启时应该已经能够产生性能差不多的代码（在异常未抛出时）。代价是产生的二进制文件大小的增加，因为异常产生的位置决定了需要如何做栈展开，这些数据需要存储在表里。典型情况，使用异常和不使用异常比，二进制文件大小会有约百分之十到二十的上升。LLVM 项目的编码规范里就明确指出这是不使用 RTTI 和异常的原因 [3]：

> 
In an effort to reduce code and executable size, LLVM does not use RTTI (e.g. `dynamic_cast&lt;&gt;;`) or exceptions.


我默默地瞅了眼我机器上 88MB 大小的单个 clang-9 可执行文件，对 Chris Lattner 的决定至少表示理解。但如果想跟这种项目比，你得想想是否值得这么去做。你的项目对二进制文件的大小和性能有这么渴求吗？需要这么去拼吗？

## 异常的问题

异常当然不是一个完美的特性，否则也不会招来这些批评和禁用了。对它的批评主要有两条：

- 异常违反了“你不用就不需要付出代价”的 C++ 原则。只要开启了异常，即使不使用异常你编译出的二进制代码通常也会膨胀。
- 异常比较隐蔽，不容易看出来哪些地方会发生异常和发生什么异常。

对于第一条，开发者没有什么可做的。事实上，这也算是 C++ 实现的一个折中了。目前的主流异常实现中，都倾向于牺牲可执行文件大小、提高主流程（happy path）的性能。只要程序不抛异常，C++ 代码的性能比起完全不做错误检查的代码，都只有几个百分点的性能损失 [4]。除了非常有限的一些场景，可执行文件大小通常不会是个问题。

第二条可以算作是一个真正有效的批评。和 Java 不同，C++ 里不会对异常规约进行编译时的检查。从 C++17 开始，C++ 甚至完全禁止了以往的动态异常规约，你不再能在函数声明里写你可能会抛出某某异常。你唯一能声明的，就是某函数不会抛出异常——`noexcept`、`noexcept(true)` 或 `throw()`。这也是 C++ 的运行时唯一会检查的东西了。如果一个函数声明了不会抛出异常、结果却抛出了异常，C++ 运行时会调用 `std::terminate` 来终止应用程序。不管是程序员的声明，还是编译器的检查，都不会告诉你哪些函数会抛出哪些异常。

当然，不声明异常是有理由的。特别是在泛型编程的代码里，几乎不可能预知会发生些什么异常。我个人对避免异常带来的问题有几点建议：

1. 写异常安全的代码，尤其在模板里。可能的话，提供强异常安全保证 [5]，在任何第三方代码发生异常的情况下，不改变对象的内容，也不产生任何资源泄漏。
1. 如果你的代码可能抛出异常的话，在文档里明确声明可能发生的异常类型和发生条件。确保使用你的代码的人，能在不检查你的实现的情况，了解需要准备处理哪些异常。
1. 对于肯定不会抛出异常的代码，将其标为 `noexcept`。注意类的特殊成员（构造函数、析构函数、赋值函数等）会自动成为 `noexcept`，如果它们调用的代码都是 `noexcept` 的话。所以，像 `swap` 这样的成员函数应当尽可能标成 `noexcept`。

## 使用异常的理由

虽然后面我们会描述到一些不使用异常、也不使用错误返回码的错误处理方式，但异常是渗透在 C++ 中的标准错误处理方式。标准库的错误处理方式就是异常。其中不仅包括运行时错误，甚至包括一些逻辑错误。比如，在说容器的时候，有一个我没提的地方是，在能使用 `[]` 运算符的地方，C++ 的标准容器也提供了 `at` 成员函数，能够在下标不存在的时候抛出异常，作为一种额外的帮助调试的手段。

```
#include &lt;iostream&gt;   // std::cout/endl
#include &lt;stdexcept&gt;  // std::out_of_range
#include &lt;vector&gt;     // std::vector
using namespace std;

```

```
vector&lt;int&gt; v{1, 2, 3};

```

```
v[0]

```

> 
`1`


```
v.at(0)

```

> 
`1`


```
v[3]

```

> 
`-1342175236`


```
try {
  v.at(3);
}
catch (const out_of_range&amp; e) {
  cerr &lt;&lt; e.what() &lt;&lt; endl;
}

```

> 
`_M_range_check: __n (which is 3) &gt;= this-&gt;size() (which is 3)`


C++ 的标准容器在大部分情况下提供了强异常保证，即，一旦异常发生，现场会恢复到调用函数之前的状态，容器的内容不会发生改变，也没有任何资源泄漏。前面提到过，`vector` 会在元素类型没有提供保证不抛异常的移动构造函数的情况下，在移动元素时会使用拷贝构造函数。这是因为一旦某个操作发生了异常，被移动的元素已经被破坏，处于只能析构的状态，异常安全性就不能得到保证了。

只要你使用了标准容器，不管你自己用不用异常，你都得处理标准容器可能引发的异常——至少有 `bad_alloc`，除非你明确知道你的目标运行环境不会产生这个异常。这对普通配置的 Linux 环境而言，倒确实是对的……这也算是 Google 这么规定的一个底气吧。

虽然对于运行时错误，开发者并没有什么选择余地；但对于代码中的逻辑错误，开发者则是可以选择不同的处理方式的：你可以使用异常，也可以使用 `assert`，在调试环境中报告错误并中断程序运行。由于测试通常不能覆盖所有的代码和分支，`assert` 在发布模式下一般被禁用，两者并不是完全的替代关系。在允许异常的情况下，使用异常可以获得在调试和发布模式下都良好、一致的效果。

标准 C++ 可能会产生哪些异常，可以查看参考资料 [6]。

## 内容小结

今天我们讨论了使用异常的理由和不使用异常的理由。希望通过本讲，你能够充分理解为什么异常是 C++ 委员会和很多大拿推荐的错误处理方式，并在可以使用异常的地方正确地使用异常这一方便的错误处理机制。

如果你还想进一步深入了解异常的话，可以仔细阅读一下参考资料 [4]。

## 课后思考

你的 C++ 项目里使用异常吗？为什么？

欢迎留言和我交流你的看法。

## 参考资料

[1] Google, “Google C++ style guide”. [https://google.github.io/styleguide/cppguide.html#Exceptions](https://google.github.io/styleguide/cppguide.html#Exceptions) 

[2] Reddit, Discussion on “Examples of C++ projects which embrace exceptions?”. [https://www.reddit.com/r/cpp/comments/4wkkge/examples_of_c_projects_which_embrace_exceptions/](https://www.reddit.com/r/cpp/comments/4wkkge/examples_of_c_projects_which_embrace_exceptions/) 

[3] LLVM Project, “LLVM coding standards”. [https://llvm.org/docs/CodingStandards.html#do-not-use-rtti-or-exceptions](https://llvm.org/docs/CodingStandards.html#do-not-use-rtti-or-exceptions) 

[4] Standard C++ Foundation, “FAQ—exceptions and error handling”. [https://isocpp.org/wiki/faq/exceptions](https://isocpp.org/wiki/faq/exceptions) 

[5] cppreference.com, “Exceptions”. [https://en.cppreference.com/w/cpp/language/exceptions](https://en.cppreference.com/w/cpp/language/exceptions) 

[5a] cppreference.com, “异常”. [https://zh.cppreference.com/w/cpp/language/exceptions](https://zh.cppreference.com/w/cpp/language/exceptions) 

[6] cppreference.com, “std::exception”. [https://en.cppreference.com/w/cpp/error/exception](https://en.cppreference.com/w/cpp/error/exception) 

[6a] cppreference.com, “std::exception”. [https://zh.cppreference.com/w/cpp/error/exception](https://zh.cppreference.com/w/cpp/error/exception) 
