noexcept 是 C++ 异常契约,承诺函数不抛异常,使编译器省略栈展开等异常处理代码,减小体积并提升性能;误用会导致 std::terminate() 静默终止;影响容器移动优化、trait 判断与重载决议,但不改变 ABI。

noexcept 能让编译器跳过异常栈展开逻辑
当函数被标记为 noexcept,编译器知道它绝不会抛出异常,于是可以省略生成异常处理相关的代码:比如栈展开(stack unwinding)的清理帧、try/catch 元信息、异常传播路径检查。这直接减少二进制体积,也避免运行时在调用点插入隐式 std::terminate() 检查分支。
常见误用场景:把可能抛异常的函数(如含 std::vector::push_back() 或 new 的构造函数)错误标为 noexcept,一旦触发会立即调用 std::terminate() —— 不是报错,而是静默终止进程,调试困难。
- 只对明确不抛异常的函数加
noexcept:如空析构函数、纯算术运算、std::move后的资源转移操作 - 移动构造/移动赋值函数若未声明
noexcept,标准容器(如std::vector)在扩容时可能退化为拷贝而非移动,性能损失显著 - 可使用
noexcept(expression)运算符做条件判断:noexcept(std::declval().swap(std::declval ()))
noexcept 不影响函数调用约定或 ABI
noexcept 是 C++ 语言层契约,不是调用约定的一部分。它不改变参数传递方式、寄存器使用或栈布局,因此不会导致 ABI 不兼容 —— 同一函数加或不加 noexcept,链接时仍能匹配符号(除非是模板实例化,此时 noexcept 是签名一部分)。
但注意:模板函数中 noexcept 约束会影响 SFINAE 和重载决议。例如 std::swap 的特化会优先选择 noexcept 版本;若你自定义了 swap 却漏了 noexcept,算法可能拒绝调用它。
立即学习“C++免费学习笔记(深入)”;
- 类成员函数的
noexcept声明属于类型系统,影响std::is_nothrow_move_constructible_v等 trait - 函数指针类型包含
noexcept属性,void(*)() noexcept和void(*)()是不同类型,不可隐式转换 - lambda 默认不带
noexcept,即使函数体为空;需显式写[]() noexcept {}
编译器实际优化效果取决于上下文
不是所有 noexcept 都能触发可观测优化。GCC/Clang 在 -O2 及以上会利用它简化调用路径,但仅当函数内联后才能彻底消除异常检查;若函数未内联,编译器仍需保留调用约定兼容性,优化有限。
典型受益场景是 STL 容器操作:比如 std::vector::resize() 内部移动元素时,会检测元素类型的移动构造是否 noexcept,决定用移动还是拷贝 —— 这个决策发生在编译期,不依赖运行时类型信息。
- 用
objdump -d或 Compiler Explorer 对比汇编,能看到noexcept函数调用前后少了call __cxa_begin_catch类指令 - MSVC 对
noexcept的优化更保守,尤其在跨模块调用时;Clang/GCC 在 LTO 模式下效果更明显 - 虚函数不能是
noexcept(C++17 起允许,但动态分发仍无法保证不抛异常),所以多态接口上慎用
noexcept(true) 和 noexcept(false) 的语义差异
noexcept 等价于 noexcept(true),表示承诺不抛;noexcept(false) 显式声明可能抛异常(等效于不写)。两者都参与重载和 trait 判断,但后者几乎无实际用途 —— 它只是强调“我可能抛”,并不禁用异常机制。
真正关键的是:一旦函数声明为 noexcept(true) 却违反承诺,程序行为是未定义的(C++ 标准要求调用 std::terminate(),但实现可自由选择是否插入检查)。
- 不要为了“看起来快”而盲目加
noexcept;宁可让函数保持默认(可能抛),也不要冒险崩溃 - 第三方库函数(如
std::string::c_str())多数已标注noexcept,可放心依赖其 trait 结果 - 自定义类型若要支持高效移动语义,至少确保移动操作和析构函数是
noexcept
最常被忽略的一点:noexcept 是接口契约,不是性能开关。它真正起作用的地方,往往不在单个函数里,而在容器、算法、模板元编程这些依赖 trait 推导的深层机制中。










