-
笔者对C++异常相关的知识较为有限,本节行文如有不妥之处,欢迎各位指出~
-
C++98风格的异常声明使用
throw(...)
,括号内是抛出异常的类型(为空代表不抛出异常)。然而实践中发现,一旦函数需要做某些更改,进而使异常类型发生变化,这种修改就会导致大量客户端调用的代码出错(笔者注:可能用了try catch捕获特定的异常)。最终大部分开发者决定干脆不要写异常声明,省得自找麻烦。 -
在建立C++11的标准中,人们逐渐达成了共识:关于异常,最有效的信息的是它是否会抛出任何异常。于是便诞生了关键字
noexcept
,它在C++11中用来声明一个函数不会抛出任何异常。带条件版的noexcept(expr)
根据括号中的表达式判断,为true时等价于无条件的noexcept
。 -
函数是否是
noexcept
是一个在设计中和const
同等重要的信息。 -
使用
noexcept
的重要好处是使编译器能产生性能更优的目标机器码。编译器对noexcept
函数不需要保持能够进行栈展开(stack unwinding)的状态,也不用考虑如果发生异常要按相反顺序摧毁对象,因此可以进行更多优化,而使用throw()
不能带来这种特性。 -
noexcept
的下一个好处与move
相关。当你想把元素push_back
到vector<Widget>
中时,如果你想利用C++11的移动语义,你自然应该为自己的Widget
类提供一个移动构造函数。push_back
有时会遇到数组元素已满的情况。C++98中的行为是分配一块新空间,逐元素地将现有内容复制过去,再逐个销毁旧空间中的元素。这种行为方式是异常安全的:如果复制过程中抛出了异常,原数组空间中的内容没有改变,因为只有当所有元素复制完成后才会进行销毁。 -
到了C++11,乍一看我们好像很自然地可以用移动构造代替复制构造,但很遗憾的是,如果移动某个元素的过程中发生了异常,此时就会进入一种“进退两难”的境地:继续构造已经进行不下去了,但原数组内容又已经被改变,若想把元素移回去还可能引发新的异常。为了继续保证
push_back
的异常安全性,C++11不能默认将复制换为移动行为,除非它知道移动操作本身不会产生异常。这种策略可以描述为 “move if you can, but copy if you must”,在std::vector::reserve
std::deque::insert
等很多函数上都有体现。而如何知道移动操作不会产生异常,答案当然就是检查它是否用noexcept
声明。(多说一点,在代码中该检查的方法是调用std::is_nothrow_move_constructible
(返回一个bool值),属于type trait,由编译器设置) -
noexcept
的另一个好处与swap
有关。swap
是STL算法实现中使用很广的一个函数,它默认也是用复制构造的。容器的swap
能不能用移动构造,一般取决于其内部存储的数据类型的移动构造是否是noexcept
的,例如std::array
和std::pair
的swap
函数声明:(注:MSVC和GCC现在似乎不是这么声明的了,而是通过type trait的std::_Is_nothrow_swappable
直接对数据类型进行判断)
template <class T, size_t N>
void swap(T (&a)[N], // see
T (&b)[N]) noexcept(noexcept(swap(*a, *b))); // below
template <class T1, class T2>
struct pair {
...
void swap(pair& p) noexcept(noexcept(swap(first, p.first)) &&
noexcept(swap(second, p.second)));
...
};
- 到这里,让我们先冷静下来。优化是很重要,但正确性是更重要的。你必须先想清楚确实会为一个函数长期提供
noexcept
的实现,再为其提供这样的声明才是合理的。一旦中途反悔试图去掉noexcept
性质,就可能破坏掉大规模客户端调用的代码。 - 实际上,大多数是函数“异常中立(exception-neutral)”的:它们自己不会抛出异常,但它们调用的函数可能会。这种情况发生时它们不会主动捕获异常而是继续向上呈递。这些函数绝对不应该被用
noexcept
声明。 - 非要为了
noexcept
性质而扭曲函数的实现方式也是不可取的(作者将其比喻为跟尾巴摇狗一样)。如果直接的实现方式可能引发异常而硬要将其隐藏(如捕获并返回状态码或特殊值),这往往不仅使你的函数实现变复杂,也会使调用处的实现变复杂,其中的成本可能早超过了noexcept
带来的优化,实属糟糕的设计。
总结
noexcept
是函数接口设计的一部分,调用者对其有依赖(也意味着不应被轻易改变)。noexcept
使函数有更大的优化空间。noexcept
对于移动,交换,内存释放和析构函数非常有价值。- 大多数函数仍是异常中立而非
noexcept
的。