5.2 KiB
Item 14:如果函数不抛出异常请使用noexcept
条款 14:如果函数不抛出异常请使用noexcept
在C++98中,异常说明(exception specifications)是喜怒无常的野兽。你不得不写出函数可能抛出的异常类型,如果函数实现有所改变,异常说明也可能需要修改。改变异常说明会影响客户端代码,因为调用者可能依赖原版本的异常说明。编译器不会为函数实现,异常说明和客户端代码中提供一致性保障。大多数程序员最终都认为不值得为C++98的异常说明如此麻烦。
在C++11标准化过程中,大家一致认为异常说明真正有用的信息是一个函数是否会抛出异常。非黑即白,一个函数可能抛异常,或者不会。这种"可能-绝不"的二元论构成了C++11异常说的基础,从根本上改变了C++98的异常说明。(C++98风格的异常说明也有效,但是已经标记为deprecated(废弃))。在C++11中,无条件的noexcept保证函数不会抛出任何异常。
关于一个函数是否已经声明为noexcept是接口设计的事。函数的异常抛出行为是客户端代码最关心的。调用者可以查看函数是否声明为noexcept,这个可以影响到调用代码的异常安全性和效率。
就其本身而言,函数是否为noexcept和成员函数是否const一样重要。如果知道这个函数不会抛异常就加上noexcept是简单天真的接口说明。
不过这里还有给不抛异常的函数加上noexcept的动机:它允许编译器生成更好的目标代码。 要想知道为什么,了解C++98和C++11指明一个函数不抛异常的方式是很有用了。考虑一个函数f,它允许调用者永远不会受到一个异常。两种表达方式如下:
int f(int x) throw(); // C++98风格
int f(int x) noexcept; // C++11风格
如果在运行时,f出现一个异常,那么就和f的异常说明冲突了。在C++98的异常说明中,调用栈会展开至f的调用者,一些不合适的动作比如程序终止也会发生。C++11异常说明的运行时行为明显不同:调用栈只是_可能_在程序终止前展开。 展开调用栈和_可能_展开调用栈两者对于代码生成(code generation)有非常大的影响。在一个noexcept函数中,当异常传播到函数外,优化器不需要保证运行时栈的可展开状态,也不需要保证noexcept函数中的对象按照构造的反序析构。而"throw()"标注的异常声明缺少这样的优化灵活性,它和没加一样。可以总结一下:
RetType function(params) noexcept; // 极尽所能优化
RetType function(params) throw(); // 较少优化
RetType function(params); // 较少优化
这是一个充分的理由使得你当知道它不抛异常时加上noexcept。
还有一些函数让这个案例更充分。移动操作是绝佳的例子。假如你有一份C++98代码,里面用到了std::vector<Widget>
。Widget通过push_back一次又一次的添加进std::vector
:
std::vector<Widget> vw;
…
Widget w;
… // work with w
vw.push_back(w); // add w to vw
假设这个代码能正常工作,你也无意修改为C++11风格。但是你确实想要C++11移动语义带来的性能优势,毕竟这里的类型是可以移动的(move-enabled types)。因此你需要确保Widget有移动操作,可以手写代码也可以让编译器自动生成,当然前提是自动生成的条件能满足(参见Item 17)。
当新元素添加到std::vector
,std::vector
可能没地方放它,换句话说,std::vector
的大小(size)等于它的容量(capacity)。这时候,std::vector
会分配一片的新的大块内存用于存放,然后将元素从已经存在的内存移动到新内存。在C++98中,移动是通过复制老内存区的每一个元素到新内存区完成的,然后老内存区的每个元素发生析构。
这种方法使得push_back
可以提供很强的异常安全保证:如果在复制元素期间抛出异常,std::vector
状态保持不变,因为老内存元素析构必须建立在它们已经成功复制到新内存的前提下。
在C++11中,一个很自然的优化就是将上述复制操作替换为移动操作。但是很不幸运,这回破坏push_back
的异常安全。如果n个元素已经从老内存移动到了新内存区,但异常在移动第n+1个元素时抛出,那么push_back
操作就不能完成。但是原始的std::vector
已经被修改:有n个元素已经移动走了。恢复std::vector
至原始状态也不太可能,因为从新内存移动到老内存本身又可能引发异常。
这是个很严重的问题,因为老代码可能依赖于push_back
提供的强烈的异常安全保证。因此,C++11版本的实现不能简单的将push_back
里面的复制操作替换为移动操作,除非知晓移动操作绝不抛异常,这时复制替换为移动就是安全的,唯一的副作用就是性能得到提升。
std::vector::push_back
受益于"如果可以就移动,如果必要则复制"策略,并且它不是标准库中唯一采取该策略的函数。