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

条款14:如果函数不抛出异常,请使用 noexcept
1. 引言在 C 中异常规范exception specification经历了从 C98 的throw()到 C11 的noexcept的演变。noexcept不仅是一种声明函数不抛出异常的方式更是现代 C 中优化代码、提升性能的重要工具。本条款将深入探讨noexcept的语义、使用场景、对编译器优化的影响以及何时应该或不应该使用它。2. noexcept 的基本语法与语义noexcept有两种基本形式无条件版本和有条件版本。// 无条件 noexcept保证函数不会抛出任何异常 void foo() noexcept; // C11 起推荐写法 void bar() noexcept(true); // 等价于 noexcept // 有条件 noexcept根据编译期条件决定是否抛出异常 templatetypename T void swap(T a, T b) noexcept(noexcept(a.swap(b)));与 C98 的throw()不同noexcept是函数类型的一部分并且违反noexcept承诺会导致程序直接调用std::terminate而不是像throw()那样可能引发std::unexpected。3. 为什么使用 noexcept3.1 性能优化编译器在生成代码时如果知道函数不会抛出异常就可以进行更多优化栈展开stack unwinding代码的省略编译器无需为可能抛出的异常生成栈展开信息减少代码体积。移动语义的启用标准库容器如std::vector在重新分配内存时如果元素的移动构造函数被标记为noexcept则会优先使用移动而非拷贝从而提升性能。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则使用拷贝以保证强异常安全即使拷贝抛出异常原容器保持不变。// 示例noexcept 如何影响 vector 的行为 std::vectorMyType v; v.reserve(10); // ... 添加元素至容量满 v.push_back(MyType{}); // 此处可能触发重分配 // 如果 MyType 的移动构造函数是 noexcept则移动否则拷贝。7. 条件 noexcept对于模板函数或依赖于模板参数的函数可以使用条件noexcept来声明异常规范。templatetypename T void swap(T a, T b) noexcept(noexcept(a.swap(b))) { a.swap(b); } // 或者使用 std::is_nothrow_swappable templatetypename T void my_swap(T a, T b) noexcept(std::is_nothrow_swappableT::value) { using std::swap; swap(a, b); }条件noexcept在编写泛型库时尤其重要它允许你根据类型特性来提供最优的异常保证。8. 检测 noexcept 与 noexcept 运算符C11 引入了noexcept运算符它是一个编译期布尔常量表达式用于检查表达式是否可能抛出异常。bool b1 noexcept(foo()); // 检查 foo() 是否声明为 noexcept bool b2 noexcept(T().swap(T())); // 检查 T 的 swap 操作是否 noexcept // 常用于条件 noexcept 声明中 templatetypename 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 高效编程的重要实践。