Item 14: 如果函数不会抛出异常就把它们声明为noexcept

本文翻译自《effective modern C++》,由于水平有限,故无法保证翻译完全正确,欢迎指出错误。谢谢!

博客已经迁移到这里啦

在C++98中,异常规范(exception specifications)是一个不稳定因素。你必须总结出一个函数可能会抛出的异常类型,所以如果函数的实现被修改了,异常规范可能也需要被修正。改变异常规范则又可能影响到客户代码,因为调用者可能依赖于原先的异常规范。编译器通常不会提供帮助来维护“函数实现,异常规范以及客户代码”之间的一致性。最终,大多数程序员觉得C++98的异常规范不值得去使用。

C++11中,对于函数的异常抛出行为来说,出现了一种真正有意义的信息,它能说明函数是否有可能抛出异常。是或不是,一个函数可能抛出一个异常或它保证它不会抛出异常。这种“可能或绝不”二分的情况是C++11异常规范的基础,这种异常规范从本质上替换了C++98的异常规范。(C++98风格的异常规范仍然是有效的,但是它们是被弃用了的。)在C++11中,无条件的noexcept就说明这个函数保证不会抛出异常。

在设计接口的时候,一个函数是不是应该这么声明(noexcept)是一个需要考虑的问题。函数的异常抛出行为是客户最感兴趣的部分。调用者能询问一个函数的noexcept状态,并且这个询问的结果能影响异常安全(exception safety)或着调用代码的性能。因此,一个函数是否是noexcept和一个成员函数是否是cosnt,这两个信息使同样重要。当你知道一个函数不会抛出异常的时候却不声明它为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的异常规范下,运行期行为稍微有些不同:调用栈只有在程序终止前才有可能被解开。

解开调用栈的时机,以及解开的可能性的不同,对于代码的产生有很大的影响。在一个noexcept函数中,如果一个异常能传到函数外面去,优化器不需要保持运行期栈为解开的状态,也不需要确保noexcept函数中的对象销毁的顺序和构造的顺序相反(译注:因为noexcept已经假设了不会抛出异常,所以就算异常被抛出,大不了就是程序终止,而不可能处理异常)。使用“throw()”异常规范的函数,以及没有异常规范的函数,没有这样的优化灵活性。三种情况能这样总结:

RetType function(params) noexcept;          //优化最好

RetType function(params) throw();           //没有优化

RetType function(params);                   //没有优化

这种情况就能作为一个充足的理由,让你在知道函数不会抛出异常的时候,把它声明为noexcept。

对于一些函数,情况变得更加强烈(更多的优化)。move操作就是一个很好的例子。假设你有一份C++98代码,它使用了std::vector。Widget通过一次次push_back来加到std::vector中:

std::vector vw;

...

Widget w;

...                     //使用w

vw.push_back(w);        //把w加到vw中

...

假设这个代码工作得很好,然后你也没有兴趣把它改成C++11的版本。但是,基于C++11的move语法能提升原来代码的性能(当涉及move-enabled类型时)的事实,你想做一些优化,因此你要保证Widget有一个move operation,你要么自己写一个,要么用函数生成器来实现(看Item 17)。

当一个新的元素被添加到std::vector时,可能std::vector剩下的空间不足了,也就是std::vector的size等于它的capacity(容量)。当发生这种事时,std::vector申请一个新的,更大的内存块来保存它的元素,然后把原来的内存块中的元素,转移到新块中去。在C++98中,转移是通过拷贝来完成的,它先把旧内存块中的所有元素拷贝到新内存块中,再销毁旧内存块中的对象(译注:再delete旧内存)。这种方法确保push_back能提供强异常安全的保证:如果一个异常在拷贝元素的时候被抛出,std::vector的状态没有改变,因为在所有的元素都成功地被拷贝到新内存块前,旧内存块中的元素都不会被销毁。

在C++11中,会进行一个很自然的优化:用move来替换std::vector元素的拷贝。不幸的是,这样做会违反push_back的强异常安全保证。如果n个元素已经从旧内存块中move出去了,在move第n+1个元素时,有一个异常抛出,push_back操作不能执行完。但是原来的std::vector已经被修改了:n个元素已经被move出去了。想要恢复到原来的状态是不太可能的,因为尝试”把新内存块中的元素move回旧内存块中“的操作也可能产生异常。

这是一个严重的问题,因为一些历史遗留代码的行为可能依赖于push_back的强异常安全的保证。因此,除非知道它不会抛出异常,否则C++11中的push_back的实现不能默默地用move操作替换拷贝操作。在这种情况(不会抛出异常)下,用move替换拷贝操作是安全的,并且唯一的效果就是能提升代码的性能。

std::vector::push_back采取”如果可以就move,不能就copy“的策略,并且在标准库中,不只是这个函数这么做。在C++98中,其他提供强异常安全的函数(比如,std::vector::reserve,std::deque::insert等等)也采取这样的策略。如果知道move操作不会产生异常,所有这些函数都在C++11中使用move操作来替换原先C++98中的拷贝操作。但是一个函数怎么才能知道move操作会不会产生异常呢?回答很明显:它会检查这个操作是否被声明为noexcept。

swap函数特别需要noexcept,swap是实现很多STL算法的关键部分,并且它也常常被拷贝赋值操作调用。它的广泛使用使得noexcept提供的优化特别有价值。有趣的是,标准库的swap是否是noexcept常常取决于用户自定义的swap是否是noexcept。举个例子,标准库中,array和std::pair的swap这么声明:

template
void swap(T (&a)[N],
          T (&a)[N])    noexcept(noexcept(swap(*a, *b)));

template
sturct pair{
    ...
    void swap(pair& p) noexcept(noexcept(swap(first, p.first)) && 
                                noexcept(swap(second, p.second)));
    ...
};

这些函数是条件noexcept(conditionally noexcept):它们是否是noexcept取决于noexcept中的表达式是否是noexcept。举个例子,给出两个Widget的数组,只有用数组中的元素来调用的swap是noexcept时(也就是用Widget来调用的swap是noexcept时),用数组调用的swap才是noexcept。反过来,这也决定了Widget的二维数组是否是noexcept。相似地,std::pair

            你要记住的事
  • noexcept是函数接口的一部分,并且调用者可能会依赖这个接口。
  • 比起non-noexcept函数,noexcept函数可以更好地被优化。
  • noexcept对于move操作,swap,内存释放函数和析构函数是特别有价值的,
  • 大部分函数是异常中立的而不是noexcept。

你可能感兴趣的:(effective,modern,c++,modern,effective,c++,翻译,翻译,effective,modern,c++)