Effective Modern C++ 条款14:如果函数不抛出异常请使用noexcept
作为C++开发者,我们都知道异常处理是语言中一个重要但复杂的特性。今天我想和大家分享一个能显著提升代码质量和性能的特性——noexcept
。
从C++98的异常说明说起
在C++98时代,异常说明是个让人又爱又恨的特性。我们需要明确列出函数可能抛出的所有异常类型:
void foo() throw(std::runtime_error, std::logic_error);
这种设计带来了很多问题:
- 函数实现改变时,异常说明也需要跟着改
- 影响客户端代码,因为调用者可能依赖原有的异常说明
- 编译器不保证函数实现与异常说明的一致性
最终,大多数开发者都放弃了这种繁琐的异常说明方式。
C++11带来的革新:noexcept
C++11引入的noexcept
从根本上改变了异常说明的方式,它只关心一个简单的问题:这个函数会不会抛出异常?
void foo() noexcept; // 保证不抛异常
void bar(); // 可能抛异常
为什么noexcept如此重要?
- 它是接口设计的一部分
noexcept
和const
一样,是函数接口的重要组成部分。调用者会根据这个信息决定如何调用你的函数。
- 性能优化
编译器会对noexcept
函数进行特殊优化:
- 不需要保证栈的可展开状态
- 不需要保证对象按构造顺序的反序析构
- 生成的代码更高效
比较以下三种声明方式:
RetType function(params) noexcept; // 极尽所能优化
RetType function(params) throw(); // 较少优化
RetType function(params); // 较少优化
- 移动语义和swap的关键
标准库容器(如std::vector
)在需要扩容时,会根据元素的移动操作是否noexcept
来决定使用移动还是复制:
class Widget {
public:Widget(Widget&& rhs) noexcept; // 移动构造函数Widget& operator=(Widget&& rhs) noexcept; // 移动赋值void swap(Widget& other) noexcept {using std::swap;swap(data, other.data);}
private:SomeType data;
};
如果移动操作不是noexcept
,容器会保守地使用复制操作,导致性能损失。
何时使用noexcept?
- 移动操作:移动构造函数和移动赋值运算符
- swap函数:成员和非成员swap函数
- 简单工具函数:确定不会抛出异常的小函数
- 内存管理:自定义的
operator delete
何时避免noexcept?
- 可能间接抛异常的函数:即使函数自己不抛异常,但调用的函数可能抛异常
- 未来可能修改的函数:如果实现可能改变并开始抛异常
- 有前置条件的函数:违反前置条件时可能需要抛异常
实际应用示例
- 标准库风格的swap
class MyType {
public:void swap(MyType& other) noexcept {using std::swap;swap(data1, other.data1);swap(data2, other.data2);}
private:int data1;std::string data2;
};void swap(MyType& a, MyType& b) noexcept {a.swap(b);
}
- 带条件的noexcept
template <typename T>
void swap(T& a, T& b) noexcept(noexcept(a.swap(b))) {a.swap(b);
}
总结
noexcept
是C++11引入的一个强大特性,正确使用它可以:
- 明确表达设计意图
- 实现更好的性能优化
- 使标准库容器更高效地使用移动语义