1. 引言
在 C++ 中,异常规范(exception specification)经历了从 C++98 的 throw() 到 C++11 的 noexcept 的演变。noexcept 不仅是一种声明函数不抛出异常的方式,更是现代 C++ 中优化代码、提升性能的重要工具。本条款将深入探讨 noexcept 的语义、使用场景、对编译器优化的影响,以及何时应该(或不应该)使用它。
2. noexcept 的基本语法与语义
noexcept 有两种基本形式:无条件版本和有条件版本。
cpp
// 无条件 noexcept:保证函数不会抛出任何异常
void foo() noexcept; // C++11 起推荐写法
void bar() noexcept(true); // 等价于 noexcept
// 有条件 noexcept:根据编译期条件决定是否抛出异常
template<typename T>
void swap(T& a, T& b) noexcept(noexcept(a.swap(b)));
与 C++98 的 throw() 不同,noexcept 是函数类型的一部分,并且违反 noexcept 承诺会导致程序直接调用 std::terminate,而不是像 throw() 那样可能引发 std::unexpected。
3. 为什么使用 noexcept?
3.1 性能优化
编译器在生成代码时,如果知道函数不会抛出异常,就可以进行更多优化:
- 栈展开(stack unwinding)代码的省略:编译器无需为可能抛出的异常生成栈展开信息,减少代码体积。
- 移动语义的启用 :标准库容器(如
std::vector)在重新分配内存时,如果元素的移动构造函数被标记为noexcept,则会优先使用移动而非拷贝,从而提升性能。
cpp
class MyType {
public:
// 移动构造函数标记为 noexcept,使 std::vector 重分配时能安全移动
MyType(MyType&& other) noexcept;
};
3.2 接口契约的明确性
noexcept 是函数接口的一部分,它向调用者明确承诺:"调用此函数不会因异常而失败"。这使得代码更容易推理,也便于编写异常安全的代码。
3.3 标准库的要求与利用
许多标准库算法和容器对操作的异常安全性有要求。例如,std::swap、std::move_if_noexcept 等设施会利用 noexcept 信息来选择更高效且安全的行为。
4. 何时应该使用 noexcept?
- 析构函数 :析构函数默认就是
noexcept的,除非显式指定为可能抛出异常(noexcept(false))。通常不应让析构函数抛出异常。 - 移动操作 :移动构造函数和移动赋值运算符应尽可能标记为
noexcept,以允许标准库容器在重分配时使用移动而非拷贝。 - 交换函数 :
swap函数通常应标记为noexcept,以保证异常安全且高效。 - 简单访问函数 :如
getter、size()、empty()等不执行复杂操作、仅返回内部状态的函数。 - 承诺不抛出的函数:如果你能 100% 确定函数及其调用的所有子函数都不会抛出异常。
5. 何时不应该使用 noexcept?
- 函数可能抛出异常 :如果函数内部调用了可能抛出异常的操作(如动态内存分配、文件 I/O、网络请求),且没有妥善处理,则不应标记
noexcept。 - 虚函数 :如果基类虚函数未声明
noexcept,则派生类重写版本也不能添加noexcept(除非基类版本本身是noexcept)。 - 无法确定的泛型代码 :在模板中,除非使用
noexcept(noexcept(...))进行条件判断,否则不应轻易添加无条件noexcept。
6. noexcept 与移动语义的紧密关系
这是 noexcept 最重要的应用场景之一。考虑 std::vector::push_back:当容量不足需要重新分配时,它需要将旧元素移动到新内存。如果元素的移动操作可能抛出异常,那么移动中途失败会导致旧状态被破坏,无法提供强异常保证。因此,std::vector 会检查移动构造函数是否标记为 noexcept:
- 如果是
noexcept,则使用移动,效率高且安全。 - 如果不是
noexcept,则使用拷贝,以保证强异常安全(即使拷贝抛出异常,原容器保持不变)。
cpp
// 示例:noexcept 如何影响 vector 的行为
std::vector<MyType> v;
v.reserve(10);
// ... 添加元素至容量满
v.push_back(MyType{}); // 此处可能触发重分配
// 如果 MyType 的移动构造函数是 noexcept,则移动;否则拷贝。
7. 条件 noexcept
对于模板函数或依赖于模板参数的函数,可以使用条件 noexcept 来声明异常规范。
cpp
template<typename T>
void swap(T& a, T& b) noexcept(noexcept(a.swap(b))) {
a.swap(b);
}
// 或者使用 std::is_nothrow_swappable
template<typename T>
void my_swap(T& a, T& b) noexcept(std::is_nothrow_swappable<T>::value) {
using std::swap;
swap(a, b);
}
条件 noexcept 在编写泛型库时尤其重要,它允许你根据类型特性来提供最优的异常保证。
8. 检测 noexcept 与 noexcept 运算符
C++11 引入了 noexcept 运算符,它是一个编译期布尔常量表达式,用于检查表达式是否可能抛出异常。
cpp
bool b1 = noexcept(foo()); // 检查 foo() 是否声明为 noexcept
bool b2 = noexcept(T().swap(T())); // 检查 T 的 swap 操作是否 noexcept
// 常用于条件 noexcept 声明中
template<typename T>
void func(T& t) noexcept(noexcept(t.some_operation())) {
t.some_operation();
}
此外,std::is_nothrow_constructible、std::is_nothrow_move_constructible 等类型特性也可用于编译期检测。
9. 实践建议与总结
- 默认不添加 :除非你能确信函数不会抛出异常,否则不要轻易添加
noexcept。错误的noexcept比没有更危险。 - 移动操作优先 :为移动构造函数和移动赋值运算符添加
noexcept,这是提升标准容器性能的关键。 - 析构函数保持默认 :不要为析构函数指定
noexcept(false)。 - 关注标准库兼容性 :如果你编写的类型可能被用于标准容器,请确保其移动操作是
noexcept的。 - 使用条件 noexcept 编写泛型代码 :在模板中,使用
noexcept(noexcept(...))来传播异常规范。 - noexcept 是接口的一部分 :一旦声明,就成为 API 契约,后续移除
noexcept可能破坏用户代码。
遵循"如果函数不抛出异常,请使用 noexcept"这一条款,不仅能提升代码性能,还能使接口意图更加清晰,是现代 C++ 高效编程的重要实践。