noexcept 是函数异常安全契约,声明后违反将直接调用 std::terminate();影响 STL 容器移动优化、重载决议、编译期验证及编译器优化,但错标会导致崩溃。

noexcept 会改变函数调用的异常安全契约
声明为 noexcept 的函数,等于向编译器和调用者做出硬性承诺:该函数内部不会抛出任何异常。一旦违反(比如调用了可能抛异常的函数而未捕获),程序将立即调用 std::terminate() 终止,不进行栈展开,也无法被 try-catch 捕获。
这不是警告或建议,而是运行时强制行为。常见踩坑点包括:
- 在
noexcept函数里直接调用未加防护的std::vector::push_back()(可能因内存分配抛std::bad_alloc) - 误以为
std::move或std::swap本身是noexcept—— 实际取决于模板参数类型是否满足std::is_nothrow_move_constructible_v等特性 - 自定义移动构造函数标记了
noexcept,但成员变量的移动操作实际可能抛异常(例如含std::string成员且其分配器抛异常)
STL 容器重分配时优先选择 noexcept 移动构造函数
std::vector、std::deque 在扩容时,若元素类型提供了 noexcept 移动构造函数,就会直接移动;否则退回到拷贝构造(即使类型有移动操作)。这直接影响性能和异常安全性。
例如:
立即学习“C++免费学习笔记(深入)”;
struct S {
std::string s;
S(S&&) noexcept; // ✅ 显式标记,且 std::string 的移动构造默认 noexcept
};
若省略 noexcept,编译器生成的移动构造函数是否 noexcept 取决于所有成员——但这个推导容易出错,显式写上更可靠。
关键点:
-
std::vector<S>::resize()扩容时,只有S(S&&) noexcept存在,才会触发移动而非拷贝 - 标准库中多数基础类型(
int、std::unique_ptr、std::string)的移动操作默认noexcept,但自定义类必须自己保证并声明 - 用
static_assert(std::is_nothrow_move_constructible_v<S>)可在编译期验证
noexcept 是函数重载和模板特化的有效区分维度
编译器把 noexcept 视为函数类型的一部分,因此可以参与重载决议和模板偏特化。
例如:
立即学习“C++免费学习笔记(深入)”;
void f() noexcept; void f() noexcept(false); f(); // 调用哪个?取决于上下文是否要求强异常安全,但通常需显式指定(如通过指针转换)
更实用的是模板场景:
template<typename T> void swap(T& a, T& b) noexcept(noexcept(a.swap(b))); // 条件 noexcept
这种写法让 swap 的异常规格自动适配 T::swap 的实际行为,避免手动维护错误。漏掉 noexcept(...) 表达式,就失去泛型健壮性。
注意:
-
noexcept修饰符差异可导致函数指针类型不兼容:void(*)()noexcept和void(*)()是不同类型 - 在回调注册、函数对象存储等场景,若接口明确要求
noexcept回调,传入非noexcept函数会编译失败
编译器优化依赖 noexcept 做出激进决策
标记 noexcept 后,编译器可省略异常处理表(.eh_frame)、跳过栈展开准备、更积极地内联或重排指令。这些优化对热路径影响明显,尤其在高频调用的小函数上。
但要注意:
- 仅当函数确实不抛异常时才加 —— 错标会导致崩溃,比性能损失严重得多
- 析构函数默认
noexcept(C++11 起),若你显式写了~T() noexcept(false),要非常清楚后果:栈展开期间析构函数再抛异常,直接终止 - 移动赋值运算符若未定义,编译器生成的版本是否
noexcept取决于所有成员的移动赋值是否noexcept,不是“默认安全”
最常被忽略的是:noexcept 不是性能开关,而是异常行为契约。它真正起作用的地方,往往不在你写的函数体里,而在标准库如何调度你的类型——尤其是容器操作和算法中对移动/交换的隐式依赖。


















