理解noexcept说明符的核心价值
noexcept说明符是C++11标准引入的关键特性,用于指定一个函数是否可能抛出异常。它不仅是一种文档化手段,更是一种承诺,编译器可以利用这个承诺进行积极的优化。从C++ Core Guidelines的角度来看,正确使用noexcept对于编写健壮、高效且易于维护的现代C++代码至关重要。函数被标记为noexcept意味着它向调用者保证,在正常执行路径上不会抛出任何异常。这种确定性允许编译器省略不必要的异常处理代码,从而可能生成更小、更快的可执行文件。
noexcept的基本语法与用法
noexcept有两种主要形式。第一种是简单的无条件的noexcept,直接在函数声明后使用`noexcept`关键字。例如:`void my_function() noexcept;`。这声明了`my_function`保证不会抛出异常。第二种是条件性的noexcept,形式为`noexcept(expression)`,其中的表达式在编译时求值,结果为true或false,以此决定函数是否被视为noexcept。例如:`void swap_my_type(MyType& other) noexcept(noexcept(other.swap(other)));`。这种形式常用于模板代码,根据模板参数的类型特性来动态决定函数的异常规格。
无条件noexcept
这是最直接的形式,适用于开发者明确知道函数实现绝不会抛出异常的情况。例如,一个只进行简单算术运算或操作栈上对象的函数。一旦声明为`noexcept`,如果函数内部意外抛出了异常,程序会立即调用`std::terminate`终止,而不是展开栈寻找异常处理器。因此,这种承诺必须是谨慎且可靠的。
条件noexcept
条件noexcept极大地增强了泛型编程的灵活性和安全性。它通常与`std::is_nothrow_constructible`、`std::is_nothrow_move_constructible`等类型特征 trait 结合使用。在编写像`std::vector`这样的容器时,移动构造函数可以标记为`noexcept(noexcept(T(std::declval())))`。这意味着,只有当类型T的移动构造函数本身是noexcept时,容器的移动构造函数才是noexcept。这允许标准库在需要提供强异常安全保证时(如`vector`的重新分配),优先使用高效且不会失败的移动操作,而不是可能失败的拷贝操作。
C++ Core Guidelines的关键建议
C++ Core Guidelines就noexcept的使用提供了一系列具体建议,其核心思想是“谨慎而明确”。
F.6: 如果您的函数可能不抛出异常,请将其声明为noexcept
这是一条基本原则。将不会抛出异常的函数标记为noexcept是一种低成本的优化手段,既为编译器提供了优化机会,也为代码使用者提供了明确的接口契约。它告诉调用者可以安全地在不允许异常抛出的上下文中(如析构函数)使用该函数。
C.85: 使移动操作noexcept
这是关于noexcept最重要和最著名的准则之一。标准库中的许多组件,特别是容器,在需要重新分配资源时(例如`std::vector::push_back`),会检查元素类型的移动构造函数和移动赋值运算符是否为noexcept。如果是,容器会优先使用移动操作以保证效率(因为移动通常比拷贝快)和强异常安全(因为移动操作不会失败)。如果移动操作不是noexcept,容器为了安全起见,可能会回退到拷贝操作,从而导致性能损失。因此,为你自定义类型的移动操作实现noexcept通常是一个好习惯。
C.80: 如果使用默认的移动操作,请确保其正确且为noexcept
当你使用`= default`来让编译器生成移动操作时,编译器通常会根据数据成员的移动操作来生成。如果所有基类和成员的移动操作都是noexcept,那么默认生成的移动操作也是noexcept。遵循此准则可以确保你的类型能够与标准库协同工作,获得最佳性能。
E.12: 绝对不要在析构函数、内存释放函数和swap函数中抛出异常,并将它们声明为noexcept
析构函数在栈展开过程中被调用,如果此时再抛出异常,程序将立即终止。因此,析构函数必须设计为不抛出异常,并且明确标记为`noexcept`。同样,`operator delete`和`swap`函数也通常被期望是异常安全的,标记为noexcept可以强化这一预期,并允许它们在关键路径上被安全使用。
noexcept的误用与风险
尽管noexcept益处良多,但误用它会带来严重风险。最核心的风险在于,将一个实际上可能抛出异常的函数错误地标记为noexcept。当这样的函数真的抛出异常时,程序会立即调用`std::terminate()`,导致不可控的崩溃,而不是按照正常的异常处理流程进行栈展开和资源清理。这会使得调试变得异常困难,并可能导致资源泄漏。因此,只有在百分之百确定函数不会抛出异常,或者函数内部的任何异常都在函数内部被捕获并处理的情况下,才应使用noexcept。
noexcept操作符
`noexcept`同时也是一个操作符(不同于说明符),用于在编译时检查一个表达式是否声明为不抛出异常。其形式为`noexcept(expression)`,返回一个bool类型的编译时常量。这个操作符在编写条件noexcept说明符和模板元编程时极其有用,如前文提到的swap示例。它允许我们根据表达式的异常规格来推导和定义我们自己的函数的异常规格。
总结
总而言之,`noexcept`是现代C++异常规格机制的核心。遵循C++ Core Guidelines来使用noexcept,能够显著提升代码的性能、可预测性和互操作性。关键在于将其视为一个严肃的承诺:只在确有把握时使用,尤其要确保移动操作、析构函数和swap函数正确标记。通过审慎地应用noexcept,开发者可以编写出更清晰、更健壮且更高效的C++程序。
1819

被折叠的 条评论
为什么被折叠?



