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
。