技术: C++11 异常争论

C++11 给出了明确的态度, 不再使用动态异常机制.

异常问题, 在 C++ 一直是个争论不休的问题, 好在C++11给出了定论.

C++11 舍弃了原来的旧的异常机制, 引入了 noexcept 机制, 简化代码, 提高性能.

本文先说一下noexcept, 再来说, 长久以来存在的异常争论.

noexcpet

noexcept, 一般代表不抛出异常或者异常传播终止, 实际上它是noexcept(true)的缩写, 保证一个函数不会抛出异常, 代替throw(),
直接终止程序, 而不去进行栈解旋或者析构对象啥的. 它提高了标准库的性能, 同时也阻止了异常扩散问题.

换句话说, 原来的动态异常声明被废弃了.

但它也可以抛出异常, 写成noexcept(false); 但是要注意捕捉时需要制定异常类型.

哪些地方会用到?

比如说容器vector, 容器deque, 这种顺序存储的, 如果空间增长不够了, 需要 memory reallocation,
此时就需要保证的容器中元素拥有noexcept类型的构造器(可能是移动构造器), 否则容器挪动元素的时候, 它就不敢调用, 从而导致重分配失败.
(实际上, 容器中vector扩容比较常见, 双端队列deque虽然是顺序存储, 但是会向两边扩展, 但是是一段一段的(多个连续存储的区块), 扩充再分一段即可)

具有move语义的函数, 一般会去保证它, 即不抛出异常.

例如

1
2
3
void foo() noexcept;
//等价于
void foo() noexcept(true); //true可以用其他条件代替.

即, 保证一个函数不会抛出异常.

或者加上条件提示:

1
2
3
4
void swap(T&x, T&y) noexcept(noexcept(x.swap(y))
{
x.swap(y);
}

即括号里面部分不抛出异常(即为true), 外层整个函数就不抛出异常.

调用端, 如果一直不处理, 最后就会交给std::terminate(), 这个函数就会调用std::abort()结束整个程序.

以前抛出具体异常的例子怎么办?

例如:

1
void *operator new(std::size_t) throw(std::bad_alloc);

现在直接写void *operator new(std::size_t) noexcept(false);即可.

其他的, 比如构函数默认就是noexcept的, 因为它不应该抛出异常. (当然你可以修改默认, 让析构抛出异常; 但delete的noexcept更改不了)

throw()

为什么抛弃了原来的动态异常声明?

我个人的理解是(并赞同), 有些运行时异常是在运行时发生的, 很难在编译时确定, 尤其是在模板代码中.

借鉴<深入理解C++11新特性与应用>作者的观点, 总结如下:

  • C++异常是运行时检查, 而不是编译时, 一旦运行失败, 会调用std::unexpected(), 且不能恢复正常流程(结果还是终止程序)
  • 运行时开销要求编译器生成更多的执行&监视代码, 阻碍优化, 增大运行时开销
  • 泛型编码中很难确定具体跑出什么类型的异常

throw()被保留了, 但是是以noexcept的形式, 保证代码段不会抛出异常.

实际上, 我们最多希望知道, 程序抛或者不抛异常(因为很难修复异常, 所以不必关心具体异常), 所以保留noexcept是非常正确的

异常争论

其实很容易想, C++已经存在很多机制帮助处理异常错误, 我们真的还需要try-catch来处理异常么?

有人是这么总结的, 异常的三境界 :

第一个境界就是:程序中看到不try,catch,finally。
这是新手的水平,他不知道有的模块/函数是会有异常抛出的,不处理的话,程序会当掉,很多资源会不能及时正确回收。或者他写程序时反复应用errno或者检查返回值的方式来处理异常情况,排错代码和正常流程代码搅在一起,混乱不堪。

第二个境界就是:程序中看到好多好多try,catch,finally。
这是入门级的水平,他懂得利用抛异常的方式来处理错误情况,所以在程序中,正常的流程会统一在try里,各种错误处理,都安排在catch当中,小心翼翼地做好的善后工作。有时候狠起来还使用catch(…)来强行把所有的异常都压下来。这样没有什么混乱?才怪,各种善后处理虽然都做了,但是他不知道要写多少个try,多少个catch,而且经常要把思路放到catch当中去。

第三个境界就是:程序中还是看到不try,catch,finally。

实际上, 你可以看到, 是否需要异常, 取决于一个很简单的事实:

出状况时, 能够正确处理(善后)

但是是否能够妥善处理出异常时的资源问题, 又会由于使用者水平不同, 所处的代码抽象层次(调用方或者提供方)需要的保证又不同.

这样的话, 就很难说, 是否需要异常了(尝试着从库的使用者和提供者分别换位思考就不难理解了)

当然高手完全可以不依赖任何外在手段, 通过代码技巧&技术解决问题, 例如Raii手法, 另当别论

Herb Sutter的《Exceptional C++》中,他将异常的安全等级划分成三个层次:

  • 基本保证(The Basic Guarantee)
    异常被抛出后,程序中剩下的所有东西都处于合法状态,没有对象或数据结构的破坏,不会发生资源泄漏现象确保出线异常时程序处于未知但有效(所谓有效,即对象的不变式检查全部通过)的状态,保证最基本的安全性。然后,程序的精确状态可能是不可预期的。
  • 强保证(The Strong Guarantee)
    确保操作的事务性满足“提交或回退语义”,即要么成功,程序处于目标状态;要么不发生改变,保持原有状态(很难)
  • 不抛出保证(The Nothrow Guarantee)
    在任何情况下都不可能引发任何异常。

析构函数、释放函数和swap函数。因为这三个函数是实现“基本保证”和“强保证”的基石。析构和释放函数不抛出保证是RAII技术有效的基本保证;
swap不抛出保证是为了“在绝不失败的过程中,把对象替换到目标状态”。

但是注意, 不抛出就不表示不处理, 抛出也不一定表示一定会处理, 最终还是要看处理的手法能否保证安全.

异常只不过是达到安全的手段之一, 没有非要和非不要的强制要求

综上
如果采用了异常机制,就尽量保证异常安全:努力实现强保证,至少实现基本保证。
常用方法有:

  • 使用超强的RAII,保证在产生异常时,资源会自动回收,实现基本保证。
  • 使用pimpl帮助实现RAII,并把逻辑操作分派到各个成员内部当中,使之在发生异常时保持一致性,通过Copy & Swap实现强保证。
  • 为析构函数、释放函数和swap函数提供最高层次的不抛出保证。
    如果不使用异常也能达到强保证, 那么异常就可以不使用.

我通常使用 raii手法 + pimpl + 智能指针时, 基本不需要异常机制 (当然基本函数还是会加上noexcept保证)

文章目录
  1. 1. noexcpet
  2. 2. throw()
  3. 3. 异常争论
|