Effective Modern C++ Item 14

Declare functions noexcept if they won’t emit exceptions

在C++98中,必须写出函数可能抛出的异常类型,如果函数实现有所改变,异常说明也可能需要修改。

从C++11开始,根据函数是否可能抛出异常或从不抛出异常的二分法来区分。noexcept保证函数不会抛出任何异常,同时允许编译器生成更好的目标代码。

noexcept允许编译器生成更好的目标代码。考虑一个不会抛出异常的函数f,两种表达方式如下:

1
2
int f(int x) throw();   //C++98风格,没有来自f的异常
int f(int x) noexcept; //C++11风格,没有来自f的异常

C++98的异常说明中,调用栈会展开至f的调用者,随后程序被终止。在C++11异常说明中的,调用栈只有在程序执行终止之前才可能展开。因此,在一个noexcept函数中,优化器不需要保证运行时栈处于可展开状态;也不需要保证noexcept函数中的对象按照构造的反序析构。所以:

1
2
3
RetType function(params) noexcept;  //极尽所能优化
RetType function(params) throw(); //较少优化
RetType function(params); //较少优化

其次, 对于某些功能, noexcept优化的机会更大。假如有一个std::vector<Widget>Widget通过push_back一次又一次的添加进std::vector

1
2
3
4
5
std::vector<Widget> vw;

Widget w;
//用w做点事
vw.push_back(w); //把w添加进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::pairswap声明如下:

1
2
3
4
5
6
7
8
9
10
template <class T, size_t N>
void swap(T (&a)[N],
T (&b)[N]) noexcept(noexcept(swap(*a, *b))); //见下文

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

一些库接口设计者会区分有宽泛契约(wild contracts)和严格契约(narrow contracts)的函数。有宽泛契约的函数没有前置条件。这种函数不管程序状态如何都能调用,它对调用者传来的实参不设约束。反之,没有宽泛契约的函数就有严格契约。对于这些函数,如果违反前置条件,结果将会是未定义的。

假如有一个形参为std::string的函数f,并且假定这个函数f决不引发异常。那么f应该被声明为noexcept。但是这个函数有一个前提条件: 传入的字符串长度不能超过32个字符。因此在窄契约中, noexcept 只是有条件的 noexcept

1
2
void f(const std::string& s) noexcept;  //前置条件:
//s.length() <= 32

重点

  • noexcept标记符是函数接口的一部分。
  • noexcept 函数比non-noexcept 函数更容易优化。
  • noexcept 对于移动语义, swap, 内存释放函数和析构函数特别有用。
  • 大多数函数都是异常中立(可能抛也可能不抛异常)的, 而不是 noexcept