Declare functions noexcept
if they won’t emit exceptions
在C++98中,必须写出函数可能抛出的异常类型,如果函数实现有所改变,异常说明也可能需要修改。
从C++11开始,根据函数是否可能抛出异常或从不抛出异常的二分法来区分。noexcept
保证函数不会抛出任何异常,同时允许编译器生成更好的目标代码。
noexcept
允许编译器生成更好的目标代码。考虑一个不会抛出异常的函数f
,两种表达方式如下:
1 | int f(int x) throw(); //C++98风格,没有来自f的异常 |
在C++98的异常说明中,调用栈会展开至f
的调用者,随后程序被终止。在C++11异常说明中的,调用栈只有在程序执行终止之前才可能展开。因此,在一个noexcept
函数中,优化器不需要保证运行时栈处于可展开状态;也不需要保证noexcept
函数中的对象按照构造的反序析构。所以:
1 | RetType function(params) noexcept; //极尽所能优化 |
其次, 对于某些功能, noexcept优化的机会更大。假如有一个std::vector<Widget>
。Widget
通过push_back
一次又一次的添加进std::vector
:
1 | std::vector<Widget> vw; |
当std::vector
的大小(size)等于它的容量(capacity),他会分配一个新的更大块的内存,并将元素从老内存区移动到新内存区,然后析构老内存区里的对象, 这就是 std::vector
实现动态扩展的方式。这种方法使得push_back
可以提供很强的异常安全保证:如果在复制元素期间抛出异常,std::vector
状态保持不变。
在C++11中,移动语义的引入使得上述复制操作能够优化为移动操作,但这会破坏push_back
的异常安全保证:异常在移动第n+1个元素时抛出,push_back
操作没有完成。但是原始的std::vector
已经被修改。
因此,std::vector::push_back
通过检查移动操作是否被声明为noexcept
。采用“如果可以就移动,如果必要则复制”策略。
swap
函数是noexcept
的另一个绝佳用地。标准库的swap
是否noexcept
有时依赖于用户定义的swap
是否noexcept
。比如,数组和std::pair
的swap
声明如下:
1 | template <class T, size_t N> |
高层次数据结构是否noexcept
取决于它的构成部分的那些低层次数据结构是否noexcept
。
大多数函数都是异常中立的,即缺少noexcept
。这些函数自己不抛异常,但是它们内部的调用可能抛出。也就是说在当前这个函数内不处理异常,但是又不立即终止程序,而是让调用这个函数的函数处理异常。
默认情况下,内存释放函数和析构函数——不管是用户定义的还是编译器生成的——都是隐式noexcept
。
一些库接口设计者会区分有宽泛契约(wild contracts)和严格契约(narrow contracts)的函数。有宽泛契约的函数没有前置条件。这种函数不管程序状态如何都能调用,它对调用者传来的实参不设约束。反之,没有宽泛契约的函数就有严格契约。对于这些函数,如果违反前置条件,结果将会是未定义的。
假如有一个形参为std::string
的函数f
,并且假定这个函数f
决不引发异常。那么f
应该被声明为noexcept
。但是这个函数有一个前提条件: 传入的字符串长度不能超过32个字符。因此在窄契约中, noexcept 只是有条件的 noexcept。
1 | void f(const std::string& s) noexcept; //前置条件: |
重点
- noexcept标记符是函数接口的一部分。
- noexcept 函数比non-noexcept 函数更容易优化。
- noexcept 对于移动语义, swap, 内存释放函数和析构函数特别有用。
- 大多数函数都是异常中立(可能抛也可能不抛异常)的, 而不是 noexcept。