代码存储位置: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
2
3
4
void f() noexcept; // 函数 f() 不抛出异常
void (*fp)() noexcept(false); // fp 指向一个可能抛出异常的函数
void g(void pfa() noexcept); // g 的参数是“不抛出异常的函数指针”
// typedef int (*pf)() noexcept; // 错误(C++17 前)

C++17 起 noexcept 说明符则属于函数类型,可作为任何函数声明符的一部分。

函数的 “抛出属性” 分类

C++ 中的每个函数要么是不抛出的(non-throwing),要么是可能抛出的(potentially throwing)

1. 可能抛出的函数

包含以下类型:

  • 声明有非空动态异常规范的函数(C++17 前);
  • 声明有 noexcept 说明符且其表达式求值为 false 的函数;
  • 未声明 noexcept 说明符的函数,但以下函数除外
    • 析构函数:除非其任何 “可能构造的基类 / 成员” 的析构函数是可能抛出的;
    • 隐式声明或首次声明时显式默认的默认构造函数、复制构造函数、移动构造函数:除非满足以下任一条件:
      1. 该构造函数的隐式定义会调用 “可能抛出的基类 / 成员构造函数”;
      2. 初始化的子表达式(如默认实参表达式)是可能抛出的;
      3. (仅默认构造函数)默认成员初始化器是可能抛出的;
    • 隐式声明或首次声明时显式默认的复制赋值运算符、移动赋值运算符:除非其隐式定义中调用的任何赋值运算符是可能抛出的;
    • 首次声明时显式默认的比较运算符(C++20 起):除非其隐式定义中调用的任何比较运算符是可能抛出的;
    • 解分配函数(deallocation functions)。

2. 不抛出的函数

除上述 “可能抛出的函数” 外的所有函数,包括:

  • noexcept 说明符表达式求值为 true 的函数;
  • 前文提到的 “例外情况” 中的析构函数、显式默认的特殊成员函数、解分配函数。

无条件形式noexcept 表示函数绝对不会抛出任何异常

1
void f() noexcept {} //f不会抛出异常,修饰全局函数和成员函数均可

条件形式noexcept(表达式) 当括号中的表达式结果为 true 时,函数不会抛出异常;为 false 时,可能抛出异常(表达式必须是编译期可计算的布尔值)。

1
2
3
void func2(int x) noexcept(x > 0) {
// 当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
2
3
4
5
6
7
8
9
10
11
12
//一起使用
void f2();
void f() noexcept(noexcept(f2())) { //f的noexcept取决于f2,f2为false,所以f为false
cout << "f() noexcept" << endl;
}
void f2() {
cout << "f() noexcept" << endl;
}
int main() {
cout << boolalpha << noexcept(f()) << endl; // false
return 0;
}

说明

  • 即使 noexcept(expr) 的结果为 true,若在对 expr 求值时遇到未定义行为expr 仍可能抛出异常。
  • 若表达式的类型为 “类类型” 或 “其(可能为多维的)数组”,则 “临时量实质化” 要求该类型的析构函数未被删除且可访问。(C++17 起)

在使用 noexcept(f()) 后并未调用 f() 函数,反而是打印出 true

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
void f() noexcept {
cout << "f() noexcept" << endl;
}

int main() {
cout << boolalpha << noexcept(f()) << endl; // true
}
void f() noexcept {
cout << "f() noexcept" << endl;
}
void f2() noexcept(false) {
cout << "f() noexcept" << endl;
}
int main() {
cout << boolalpha << noexcept(f()) << endl; // true
cout << boolalpha << noexcept(f2()) << endl; // true
}
void f() noexcept {
cout << "f() noexcept" << endl;
}
void f2() noexcept(false) {
cout << "f() noexcept" << endl;
}
void f3() {
cout << "f() noexcept" << endl;
}
int main() {
cout << boolalpha << noexcept(f()) << endl; // true
cout << boolalpha << noexcept(f2()) << endl; // false
cout << boolalpha << noexcept(f3()) << endl; // false
}

无论写不写 noexcept(false),他都表示会抛出异常。

在 C++17 中,noexcept 有改动

1
2
3
4
5
6
7
void f() noexcept {
cout << "f() noexcept" << endl;
}

int main() {
auto p = f;
}

image.png

不求值表达式

以下操作数是未求值操作数,它们不被求值

  • typeid 运算符作用的表达式,除了多态类类型的泛左值
  • sizeof 运算符的操作数的表达式
  • noexcept 运算符的操作数
  • decltype 说明符的操作数

除非运算符是 typeid 且操作数是多态泛左值,因为这些运算符只会查询他们操作数的编译性质,因此 std::size_t n = sizeof(std::cout << 42); 不进行控制台输出。

1
2
3
4
5
int main() {
int i = 1;
noexcept(i++);
cout << i << endl; // 1
}

很明显输出 i 仍然为 1,即使将 i++ 更改为 std::cout 相关操作,仍然不会有任何输出。

使用 noexcept

对于确定不会抛出异常的函数,显式标记 noexcept 以提升性能和明确接口语义

1. 析构函数(默认 noexcept

析构函数默认隐式为 noexcept(除非用户显式声明可能抛异常),但显式标记可增强可读性:

1
2
3
4
5
6
7
8
9
10
class FileHandler {
public:
~FileHandler() noexcept { // 显式声明,明确不抛异常
if (file_) {
fclose(file_); // fclose 通常不抛异常
}
}
private:
FILE* file_ = nullptr;
};

若析构函数可能抛异常(极罕见),需显式声明 noexcept(false),否则编译器会视为 noexcept,抛出异常时程序会终止。

2. 移动操作(移动构造 / 赋值)

移动操作(move constructormove assignment)是 noexcept 的重要应用场景。标准容器(如 std::vector)在扩容时,若元素的移动操作是 noexcept,会优先使用更高效的移动而非复制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class MyString {
public:
// 移动构造函数:资源转移,不抛异常
MyString(MyString&& other) noexcept
: data_(other.data_), size_(other.size_) {
other.data_ = nullptr; // 避免源对象析构时释放资源
other.size_ = 0;
}

// 移动赋值运算符:同样不抛异常
MyString& operator=(MyString&& other) noexcept {
if (this != &other) {
delete[] data_; // 释放当前资源
data_ = other.data_; // 接管源对象资源
size_ = other.size_;
other.data_ = nullptr;
other.size_ = 0;
}
return *this;
}

// 复制操作:可能抛异常(内存分配失败),不标记 noexcept
MyString(const MyString& other) {
size_ = other.size_;
data_ = new char[size_]; // new 可能抛 bad_alloc
memcpy(data_, other.data_, size_);
}

private:
char* data_ = nullptr;
size_t size_ = 0;
};

std::vector<MyString> 扩容时,会检测到 MyString 的移动构造是 noexcept,从而使用移动而非复制,提升性能。

3. 工具函数

对于逻辑简单、明确不会抛异常的工具函数,标记 noexcept 可帮助编译器优化:

1
2
3
4
5
6
7
8
9
// 计算两数之和(纯算术操作,不抛异常)
int add(int a, int b) noexcept {
return a + b;
}

// 检查指针是否为空(无异常风险)
bool is_nullptr(const void* ptr) noexcept {
return ptr == nullptr;
}

4. 条件式 noexcept(依赖其他操作的异常特性)

当函数的异常行为依赖于其内部调用的函数时,使用 noexcept(表达式) 动态指定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Container {
private:
std::vector<int> data_;

public:
// 若 vector 的 swap 不抛异常,则当前 swap 也不抛
void swap(Container& other) noexcept(noexcept(data_.swap(other.data_))) {
data_.swap(other.data_); // vector::swap 是 noexcept 的
}
};

// 模板函数:根据 T 的 operator+ 是否抛异常,决定自身是否 noexcept
template <typename T>
T sum(const T& a, const T& b) noexcept(noexcept(a + b)) {
return a + b;
}

noexcept(noexcept(a + b)) 中,内层 noexcept(a + b) 是一个运算符,用于编译期检查 a + b 是否可能抛异常(返回 bool),外层 noexcept(...) 根据该结果决定函数是否标记为 noexcept

但是,当函数内部可能调用抛异常的操作(如 newdynamic_cast、标准库中可能抛异常的函数),或不确定是否会抛异常的函数(宁可不标记,也不要错误标记)时,不要滥用 noexcept