为不会抛出异常的函数添加noexcept声明
在C++11以前,你必须梳理出一个函数可能抛出的所有异常类型,如果函数实现做了改动,那么异常规格也要进行修订,而异常的改动可能会破坏客户端代码,调用方可能依赖于原先的异常类型。而在C++11以后,逐渐形成了一个共识,关于函数抛出异常这个事情,最重要的信息是它到底会不会抛出异常,而非抛出异常的类型是什么。这种非黑即白的思想,C++11引入了noexcept关键字,无条件的noexcept就是为了不会抛出异常的函数而准备的。
我们可以对比一下C++98和C++11在表达函数不会抛出异常时的表达方式
int func(int x) throw() ; // C++98
int func(int x) noexcept ; // C++11
如果在运行期,一个异常超出func的作用域,则func的异常类型被违反,C++98的异常规格下调用栈会展开至func的调用方,然后执行一些操作后程序执行中止。而在C++11中,程序执行中止前,调用栈只是可能会展开。在带有noexcept声明的函数中,优化器不需要在异常传出函数的前提下,将执行期栈保持在开解状态,也不需要在异常溢出函数的前提下,保证所有其中的对象以其构造顺序的逆序完成析构,而以throw()的声明就得不到这样的优化。
C++11中的移动语句是需要noexcept的典型例子之一,如果你有一段代码,使用一个std::vector的对象,通过push_back向其中添加对象
std::vector<Widget> vw;
Widget w;
vw.push_back(w);
当向std::vector对象中添加新元素的时候,如果size和capacity相等时,std::vector会分配一块新的,更大的内存块(一般是原先的2倍)来存储其元素,C++98中,转移原有元素的做法是逐个的从旧内存复制到新内存,然后析构旧内存中的元素,这个做法使得push_back能够提供强异常安全保证,如果复制过程中抛出了异常,std::vector对象会保持原样不变。而在C++11引入移动构造后,复制操作就变成了移动操作,这样做违反了push_back的强异常安全保证(如果前n个元素以及从旧内存中移出,第n+1个元素抛出异常的时候,前n个元素已经被修改了,不能够恢复到原始状态了)
这是个严重的问题,遗留代码可能会依赖于push_back的强异常安全保证,所以C++11中的实现就是使移动操作不会抛出异常,在这个前提下,上述的情况就是安全的。
swap函数是及其需要noexcept声明的另一个例子。swap函数是许多STL算法实现的核心组件。在复制赋值运算符中,它也经常被调用,有意思的是,标准库中的swap函数是否带有noexcept声明取决于用户定义的swap函数是否带有noexcept声明。例如标准库为数组和std::pair提供的swap函数如下:
// 数组
template<class T, size_t N>
void swap(T (&a)[N], T (&b)[N]) noexcept(noexcept(swap(*a, *b)));
// std::pair
template<class T1, class T2>
struct pair {
void swap(pair& p) noexcept(noexcept(swap(first, p.first)) && noexcept(swap(second, p.second)));
};
这些函数都带有条件时的noexcept声明,通过对条件式的noexcept分句中的表达式来判断结果是否为noexcept。
事实上,大多数函数都是异常中立的。此类函数自身并不抛出异常,但是他们调用的函数可能会发射异常,当这种情况真的发生时,异常中立的函数会允许该发射的异常传到调用栈的更深异常,异常中立的函数永远不具备noexcept的性质,所以大多数函数天生就担不起noexcept的称号。
值得注意的是,对于某些函数来讲,具备noexcept是非常重要的,尤其是和内存释放有关的函数,在C++11中,内存释放函数和所有的析构函数(无论是用户自己定义的还是编译器自动生成的)都隐含地具备noexcept性质,这么依赖,它们无需加上noexcept声明了。
最后一点,我们讨论一下编译器一般对于识别函数实现及其异常规格的一致性不予支持的问题
void setup();
void cleanup();
void doWork() noexcept {
setup() // 建立要做的工作
//... 做工作
cleanup(); // 清理工作
}
这里的doWork带有noexcept声明,尽管它调用了不带noexcept声明的函数setup和cleanup,这看起来自相矛盾,但是也有可能是setup和cleanup已经在文档中说明它们不会抛出异常。这里不加声明也可能有其理由,比如来自C语言撰写的库函数等等,这种情况下,编译器会假设这些函数不会抛出异常,也不会对此生成警告。
总结:
- noexcept声明是函数接口的组成部分,这意味着调用方可能会对它有依赖
- 对于不带noexcept声明的函数,带有noexcept声明的函数可以得到编译器更好的优化
- noexcept性质对于移动操作,swap,内存释放,析构函数最有价值
- 大多数函数是异常中立的,不具备noexcept性质
参考:《Modern Effective C++》第五版