条款14:如果函数不抛出异常,请使用 noexcept

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::swapstd::move_if_noexcept 等设施会利用 noexcept 信息来选择更高效且安全的行为。

4. 何时应该使用 noexcept?

  • 析构函数 :析构函数默认就是 noexcept 的,除非显式指定为可能抛出异常(noexcept(false))。通常不应让析构函数抛出异常。
  • 移动操作 :移动构造函数和移动赋值运算符应尽可能标记为 noexcept,以允许标准库容器在重分配时使用移动而非拷贝。
  • 交换函数swap 函数通常应标记为 noexcept,以保证异常安全且高效。
  • 简单访问函数 :如 gettersize()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_constructiblestd::is_nothrow_move_constructible 等类型特性也可用于编译期检测。

9. 实践建议与总结

  • 默认不添加 :除非你能确信函数不会抛出异常,否则不要轻易添加 noexcept。错误的 noexcept 比没有更危险。
  • 移动操作优先 :为移动构造函数和移动赋值运算符添加 noexcept,这是提升标准容器性能的关键。
  • 析构函数保持默认 :不要为析构函数指定 noexcept(false)
  • 关注标准库兼容性 :如果你编写的类型可能被用于标准容器,请确保其移动操作是 noexcept 的。
  • 使用条件 noexcept 编写泛型代码 :在模板中,使用 noexcept(noexcept(...)) 来传播异常规范。
  • noexcept 是接口的一部分 :一旦声明,就成为 API 契约,后续移除 noexcept 可能破坏用户代码。

遵循"如果函数不抛出异常,请使用 noexcept"这一条款,不仅能提升代码性能,还能使接口意图更加清晰,是现代 C++ 高效编程的重要实践。

相关推荐
华科大胡子8 天前
条款13:优先选用const_iterator,而非iterator
iterator·modern c++·const_iterator
华科大胡子1 个月前
条款08:优先选用nullptr,而非0和NULL
nullptr·null·modern c++·空指针
华科大胡子1 个月前
条款06(缺点):当auto推导的类型不符合要求时,使用显式类型初始化惯用法
auto·modern c++·类型推导
七夜zippoe2 个月前
DolphinDB异常检测引擎:实时告警
java·服务器·网络·异常·告警·dolphindb
华科大胡子2 个月前
条款05(优点):优先考虑auto类型推导,而非显式类型声明
闭包·auto·modern c++·类型推导
香吧香2 个月前
Jstack定位生产环境线程阻塞问题解决
jvm·异常
是个西兰花3 个月前
C++:异常
开发语言·c++·异常
xiaoye-duck3 个月前
【C++:异常】C++ 异常讲解指南:从理论到实践,深入理解栈展开和优雅处理程序错误
开发语言·c++·异常
量子炒饭大师3 个月前
【C++11】RAII 义体加装指南 ——【包装器 与 异常】C++11中什么是包装器?有哪些包装器?C++常见异常有哪些?(附带完整代码讲解)
开发语言·c++·c++11·异常·包装器