更新时间: 2026-08-27
前面几篇反复出现一个词:noexcept。析构函数"默认就是 noexcept"、swap/移动操作"应该声明 noexcept"。这篇把它单独拎出来讲透——它是什么、怎么写、对性能和正确性分别意味着什么。
本文要回答:noexcept 到底声明了什么?为什么析构、移动、swap 要标它?它真的能提升性能吗?
一、noexcept 是什么
noexcept 是 C++11 起的异常说明符,放在函数声明末尾,承诺"这个函数不会抛出异常":
void swap_ints(int& a, int& b) noexcept {
int t = a;
a = b;
b = t; // 纯整型操作,不可能抛异常
}如果标了 noexcept 的函数实际抛了异常呢?程序不会"优雅地"传播这个异常——而是直接调用 std::terminate 终止。所以 noexcept 是双刃剑:它既是给编译器的承诺,也是给自己立下的规矩——"这里绝不能抛,抛了就是程序级错误"。
noexcept 还有带条件的版本:noexcept(表达式),当表达式为 true 时承诺不抛。最常见的使用是"转发类型特性":
template <typename T>
void f(T& x) noexcept(noexcept(x.swap(x))) {
x.swap(x); // 只有 swap 也不抛时,f 才承诺不抛
}这个属于模板进阶,入门阶段知道"noexcept 可以带条件"就够了。
二、默认与例外
一个容易混淆的点:哪些函数默认 noexcept?
| 函数 | 默认 noexcept? |
|---|---|
| 析构函数 | 是(C++11 起默认 noexcept) |
| 普通成员/自由函数 | 否 |
| 默认构造/拷贝构造/赋值 | 条件性(依赖成员是否 noexcept) |
| 移动构造/移动赋值 | 条件性(依赖成员) |
所以 56 篇说的"析构不能抛异常"其实有语言层面背书——析构函数默认就是 noexcept,你写 ~X() {} 不标任何东西,它已经不承诺抛异常了。如果析构函数体里真的 throw,程序照样 terminate。
struct X {
~X() {} // 默认 noexcept(true)
void f(); // 默认 noexcept(false)(可以抛)
void g() noexcept; // 显式承诺不抛
};三、为什么移动和 swap 要标 noexcept
这是工程里最重要的一条惯例,和 std::vector 的扩容行为直接相关。
看 std::vector 扩容时发生了什么(32 篇讲过):容量不够就重新分配内存,把旧元素"搬"到新内存。搬的时候用移动构造还是拷贝构造,取决于移动构造是否 noexcept:
- 如果移动构造标了
noexcept:扩容直接用移动(快,只是把指针/内部状态换过去) - 如果移动构造可能抛异常:扩容必须退回拷贝(因为移动到一半抛异常,旧数据已经乱了,无法回滚)
@startuml
left to right direction
skinparam nodeFontSize 13
skinparam backgroundColor #FFFFFF
rectangle "vector 扩容搬元素" as vec #E8F1FF {
if (移动构造 noexcept?) then (是)
node "移动:只搬内部状态,快" as mv #D9FFE2
else (否)
node "拷贝:安全但慢(可能深拷贝)" as cp #FFF3D6
endif
}
note bottom of vec
移动不标 noexcept,
vector 宁可拷贝也不用移动——
这是标准库的"强异常安全"选择
end note
@enduml一个具体的性能对照——自定义类存进 vector:
#include <vector>
struct Movable {
Movable(Movable&&) noexcept {} // 标了 noexcept:扩容用移动
};
struct NotSafe {
NotSafe(NotSafe&&) {} // 没标:扩容用拷贝(如果可拷贝)
};所以自定义类如果要放进 std::vector 且成员移动很快(指针、整数、string 之类),移动构造/移动赋值/swap 都标 noexcept 是标准做法——它直接影响容器扩容的性能。标准库自己的容器、std::string 的移动操作全都是 noexcept 的。
四、noexcept 对性能的影响
noexcept 对性能的影响有两层:
1. 优化层面(间接):编译器知道函数不抛异常,就能省掉"异常处理边界的登记"(unwind tables、异常元数据),生成更干净的代码。这个收益在小型、高频函数上最明显。但不是魔术——-O2 下编译器自己也能推断一部分"显然不抛"的函数,noexcept 是显式告诉它。
2. 语义层面(关键):如上节所述,标准库的容器操作会看移动/swap 是否 noexcept 来选策略。这不是性能优化,是行为选择——标了,容器敢用移动;没标,容器保守用拷贝。这个影响远比"寄存器优化"大。
顺带一提:std::vector 的 shrink_to_fit、std::swap 泛型版本等,标准库都会利用 noexcept 信息。所以 noexcept 是"写给别人看的承诺",别人(标准库、你的队友)依据它做决策。
五、什么时候该标 noexcept
该标:
- 析构函数(默认已是,别画蛇添足写
noexcept,写了也一样) - 移动构造/移动赋值(成员都可安全移动时)
swap(大多数情况)- 纯查询/纯计算函数(只读成员、纯算法)
- 给
std::vector用的自定义类型的移动操作
不该标:
- 可能分配内存的函数(
new/容器操作可能抛bad_alloc) - 可能抛业务异常的函数
- 调用了"不承诺 noexcept"的函数(除非用 try/catch 包住)
别拿 noexcept 当"性能开关"乱贴——标错了(实际会抛),异常发生时不是"优雅传播"而是 terminate 崩掉。noexcept 是契约,不是装饰。
六、与 C 对照
C 没有异常,所以也没有 noexcept——但 C 有类似的对立物:noreturn(C11)和"函数指针类型是否允许调用"这类约定。
_Noreturn void fatal_error(void) { // C11:承诺不返回
abort();
}| 维度 | C | C++ |
|---|---|---|
| "不抛异常"承诺 | 无此概念 | noexcept |
| "不返回"承诺 | _Noreturn(C11) | [[noreturn]] |
| 违反承诺 | — | std::terminate |
| 编译器利用 | — | 优化 + 容器策略选择 |
C 的错误处理是返回码,不存在"异常规范"问题;C++ 因为有了异常,才需要 noexcept 这类声明来让编译器和其他代码做决策。
七、与本站主线衔接
- 54-56 篇:异常的完整机制,noexcept 是"异常规范的收尾"
- 42/43/44 篇:智能指针的移动操作都是 noexcept 的,放进容器天然高效
- 20 篇(拷贝/Rule of Three):自定义类若加移动操作,noexcept 是该一起考虑的
- 32 篇(vector 扩容):noexcept 影响扩容策略的具体场景
- 高手层
exception-safety.md:异常安全级别与 noexcept 的关系
八、一句话总结
noexcept 声明"此函数不抛异常",违反会直接 std::terminate;析构默认 noexcept,移动构造/移动赋值/swap 标 noexcept 是工程惯例——它不只关乎优化,更决定 std::vector 扩容敢不敢用移动,是"写给标准库和队友的承诺"。