Noxcept
代码存储位置:shanchuann/Modern_CPP
noexcept 说明符(C++11 起)
noexcept 是一个异常说明符(exception specifier),用于明确指定函数是否可能抛出异常。
语法
| 语法形式 | 编号 | 说明 |
|---|---|---|
noexcept |
(1) | |
noexcept(表达式) |
(2) | |
throw() |
(3) | (C++17 中弃用,C++20 中移除) |
- (1) 等同于
noexcept(true) - (2) 若表达式求值为
true,则函数声明为不抛出任何异常。noexcept后的(始终是此形式的一部分(它绝不能用于开始初始化器)。 - (3) 等同于
noexcept(true)(关于其在 C++17 之前的语义,见 “动态异常规范”)。
表达式:可按上下文转换为 bool 类型的常量表达式。
在 C++17 前,noexcept 说明符不属于函数类型(与动态异常规范类似),仅能用于以下场景:
作为 lambda 声明符的一部分;
作为 “顶级函数声明符” 的一部分 —— 用于声明函数、变量、函数类型的非静态数据成员、函数指针、函数引用、成员函数指针时;
在这些声明中声明 “本身是函数指针 / 引用” 的参数或返回类型时。它不能出现在 typedef 或类型别名声明中。
1 | void f() noexcept; // 函数 f() 不抛出异常 |
C++17 起 noexcept 说明符则属于函数类型,可作为任何函数声明符的一部分。
函数的 “抛出属性” 分类
C++ 中的每个函数要么是不抛出的(non-throwing),要么是可能抛出的(potentially throwing):
1. 可能抛出的函数
包含以下类型:
- 声明有非空动态异常规范的函数(C++17 前);
- 声明有
noexcept说明符且其表达式求值为false的函数; - 未声明
noexcept说明符的函数,但以下函数除外:- 析构函数:除非其任何 “可能构造的基类 / 成员” 的析构函数是可能抛出的;
- 隐式声明或首次声明时显式默认的默认构造函数、复制构造函数、移动构造函数:除非满足以下任一条件:
- 该构造函数的隐式定义会调用 “可能抛出的基类 / 成员构造函数”;
- 初始化的子表达式(如默认实参表达式)是可能抛出的;
- (仅默认构造函数)默认成员初始化器是可能抛出的;
- 隐式声明或首次声明时显式默认的复制赋值运算符、移动赋值运算符:除非其隐式定义中调用的任何赋值运算符是可能抛出的;
- 首次声明时显式默认的比较运算符(C++20 起):除非其隐式定义中调用的任何比较运算符是可能抛出的;
- 解分配函数(deallocation functions)。
2. 不抛出的函数
除上述 “可能抛出的函数” 外的所有函数,包括:
noexcept说明符表达式求值为true的函数;- 前文提到的 “例外情况” 中的析构函数、显式默认的特殊成员函数、解分配函数。
无条件形式:noexcept 表示函数绝对不会抛出任何异常。
1 | void f() noexcept {} //f不会抛出异常,修饰全局函数和成员函数均可 |
条件形式:noexcept(表达式) 当括号中的表达式结果为 true 时,函数不会抛出异常;为 false 时,可能抛出异常(表达式必须是编译期可计算的布尔值)。
1 | void func2(int x) noexcept(x > 0) { |
与旧特性 throw() 的区别
C++98 中使用 throw() 声明函数不抛异常(如 void func() throw();),但 noexcept 是其更高效的替代者:
noexcept是编译期检查,而throw()会在运行时检查(若抛出异常会调用std::unexpected)。noexcept的优化能力更强:编译器知道函数不抛异常后,可省略异常处理的额外代码(如栈展开准备)。- C++11 后
throw()被标记为 “弃用”,C++17 中正式移除。
noexcept 运算符(C++11 起)
语法
返回类型为 bool 的纯右值(prvalue)。其结果规则如下:
- C++17 之前:若表达式的 “潜在异常集” 为空,则结果为
true;否则为false。 - C++17 起:若表达式被指定为 “不抛出异常”,则结果为
true;否则为false。
补充说明:
- 上述 “表达式” 是未求值操作数(即编译时仅检查其异常声明,不实际执行表达式)。
- 若表达式为纯右值(prvalue),则会应用 “临时量实质化”(temporary materialization)。(C++17 起)
1 | //一起使用 |
说明
- 即使
noexcept(expr)的结果为true,若在对expr求值时遇到未定义行为,expr仍可能抛出异常。 - 若表达式的类型为 “类类型” 或 “其(可能为多维的)数组”,则 “临时量实质化” 要求该类型的析构函数未被删除且可访问。(C++17 起)
在使用 noexcept(f()) 后并未调用 f() 函数,反而是打印出 true
1 | void f() noexcept { |
无论写不写 noexcept(false),他都表示会抛出异常。
在 C++17 中,noexcept 有改动
1 | void f() noexcept { |

不求值表达式
以下操作数是未求值操作数,它们不被求值
除非运算符是 typeid 且操作数是多态泛左值,因为这些运算符只会查询他们操作数的编译性质,因此 std::size_t n = sizeof(std::cout << 42); 不进行控制台输出。
1 | int main() { |
很明显输出 i 仍然为 1,即使将 i++ 更改为 std::cout 相关操作,仍然不会有任何输出。
使用 noexcept
对于确定不会抛出异常的函数,显式标记 noexcept 以提升性能和明确接口语义。
1. 析构函数(默认 noexcept)
析构函数默认隐式为 noexcept(除非用户显式声明可能抛异常),但显式标记可增强可读性:
1 | class FileHandler { |
若析构函数可能抛异常(极罕见),需显式声明
noexcept(false),否则编译器会视为noexcept,抛出异常时程序会终止。
2. 移动操作(移动构造 / 赋值)
移动操作(move constructor、move assignment)是 noexcept 的重要应用场景。标准容器(如 std::vector)在扩容时,若元素的移动操作是 noexcept,会优先使用更高效的移动而非复制。
1 | class MyString { |
当 std::vector<MyString> 扩容时,会检测到 MyString 的移动构造是 noexcept,从而使用移动而非复制,提升性能。
3. 工具函数
对于逻辑简单、明确不会抛异常的工具函数,标记 noexcept 可帮助编译器优化:
1 | // 计算两数之和(纯算术操作,不抛异常) |
4. 条件式 noexcept(依赖其他操作的异常特性)
当函数的异常行为依赖于其内部调用的函数时,使用 noexcept(表达式) 动态指定:
1 | class Container { |
noexcept(noexcept(a + b)) 中,内层 noexcept(a + b) 是一个运算符,用于编译期检查 a + b 是否可能抛异常(返回 bool),外层 noexcept(...) 根据该结果决定函数是否标记为 noexcept。
但是,当函数内部可能调用抛异常的操作(如 new、dynamic_cast、标准库中可能抛异常的函数),或不确定是否会抛异常的函数(宁可不标记,也不要错误标记)时,不要滥用 noexcept。





