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 | void foo() noexcept; |
即, 保证一个函数不会抛出异常.
或者加上条件提示:
1 | void swap(T&x, T&y) noexcept(noexcept(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保证)