Effective Modern C++ Item 14 只要函数不会发射异常,就为其加上noexcept声明

在C++98中,异常规格是一个喜怒无常的野兽。一般来说,不要招惹为好。

而在C++11中,逐渐达成了一个共识:

关于异常发射这个事情,关键在于是否会有异常,而不关注型别。

所以当你知道这个函数不会发射异常,那么就应该给这个函数加上noexcept声明,否则这是一个接口规格缺陷。

动机:让编译器更好的生成目标代码

参考差异代码:

int f(int x) throw();       //f不会发射异常:C++98风格
int f(int x) noexcept;      //f不会发射异常:C++11风格

如果在运行期,一个异常逸出f的作用域,则f的异常规格被违反。在C++98的异常规格下,调用栈会开解至f的调用方,然后执行一些操作,程序中止。但在C++11的规格下,运行期行为会略有不同,程序执行中止之前,栈只是可能会开解

这个一定会开解和可能会开解,对代码生成的差异实际上比想象中要更大。

在带有noexcept声明的函数中:

  • 优化器不要在异常传出函数的前提下,将执行期栈保持在可开解状态

  • 不需要在异常逸出函数的前提下,保证所有其中的对象以其被构造顺序的逆序完成析构

而那些以throw()异常规格声明的函数享受不到这样的优化灵活性。

RetType function(params) noexcept;  //最优化
RetType function(params) throw();   //优化不够
RetType function(params);           //优化不够

经典例子:移动操作

移动操作就是一个极好的例子。假如有一段C++98代码,使用一个std::vector<Widget>型别对象,而且会用push_back往其中添加一些Widget对象。实现代码可能如下:

std::vector<Widget> vw;
...
Widget w;           //使用w
...
vw.push_back(w);    //将w加入vw

std::vector型别对象中添加新元素,可能会空间不够。假设size() == capacity()时,再要加入新元素,那么这个事件发生的时候,std::vector会进行会重新分配,然后将现有元素移动到新位置(加入新元素个数小于当前个数则按照当前容量翻倍,如果大于当前容量,则新空间等于当前容量+新增容量)。

  • C++98中,转移的做法是将元素逐个从旧内存复制到新内存,然后将旧内存中对象析构。这样提供了强异常安全保证如果在复制过程中抛出异常,原对象会保持原样不变

  • C++11中,一个自然的优化,针对std::vector型别元素复制换成移动操作。但不幸的是,这样违反了push_back的强异常安全保证。如果移动过程中出现异常,那么,原来的对象就会已经被移走一部分从而无法恢复。

而实际上在C++11中是按照如果移动操作不会发射异常,则进行移动。也就是能移动则移动,必须复制才复制的策略。而STL库中,std::vector::reserve, std::deque::insert这些也都是采用的这种方式,这种方式的基础就是查看移动操作是否带有noexcept声明。

经典例子:swap操作

swap函数是许多STL算法的核心组件。在复制赋值运算符中,也被常常调用。它的广泛使用意味着针对它声明noexcept的声明也是收益可观的。

std::swap的异常声明取决于用户定义的swap函数是否带有noexcept声明。例如标准库为数组std::pair准备的swap函数如下:

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)));
...
};

这里存在这样一个事实:

高阶数据结构的swap行为要noexcept性质,一般地,仅当构建它的低阶数据结构具备noexcept性质时才成立。

以上性质有一个推论,只要有可能的情况下,都让函数带有noexcept声明。但需要说明的是,如果声明了noexcept但是函数会抛出异常,那么遇到这种情况程序则会直接退出。所以优化诚可贵,正确价更高

一个事实:大多数函数是异常中立的

大多数函数是异常中立的,他们不产生异常,但是他们调用的函数可能会产生异常。异常中立函数永远不具备noexcept性质

一个特点:有部分函数具备noexcept性质后,收益会很高

  • 移动函数

  • swap函数

但是需要强调的一点是,不要刻意将函数异常内部捕获从而达到对外noexcept的效果,这样带来的收益会远远小于维护成本。注意需要自然(非强制try catch)的实现。

一个新规则

C++98允许operator delete或者operator delete[]和析构函数抛出异常,但这被认为是一种差劲的编程风格。

C++11修改了这个规则,默认的内存释放函数和所有的析构函数都隐式具备noexcept性质。如果这两类函数中抛出异常,则会产生未定义行为

宽松契约函数,狭隘契约函数

宽松契约函数:传入参数不用不关心程序当前状态。
狭隘契约函数:传入的参数需要满足一定规则,否则会行为未定义。例如:

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

一般来说,只对宽松契约函数声明noexcept,所以上述写法是不合适的。

最后一个用法介绍

void setup();
void cleanup();

void doWork() noexcept
{
    setup();
    ...
    cleanup();
}

注意到虽然setupcleanup不带noexcept但是doWork有,这看起来自相矛盾,但的确是合适的。因为setupcleanup可能是从C++98,或者C代码中引用过来的函数。所以只要的确是不会有异常逸出,那么就可以加上noexcept,编译器是不会对此情况进行报错的。

要点速记
1. noexcept声明是函数接口的组成部分,这意味着调用方可能会对它有依赖。
2. 相对于不带noexcept声明的函数,带有noexcept声明的有更多机会会被优化。
3. noexcept性质对于移动,swap,内存释放,析构函数最有价值。
4. 大多数函数都是异常中立的,不具备noexcept性质。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值