《Effective Modern C++》学习笔记 - Item 14: 对不会抛出异常的函数使用 noexcept 声明

  • 笔者对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_backvector<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::arraystd::pairswap 函数声明:(注: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 带来的优化,实属糟糕的设计。

总结

  1. noexcept 是函数接口设计的一部分,调用者对其有依赖(也意味着不应被轻易改变)。
  2. noexcept 使函数有更大的优化空间。
  3. noexcept 对于移动,交换,内存释放和析构函数非常有价值。
  4. 大多数函数仍是异常中立而非 noexcept 的。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值