Skip to content
Gains Summary
Main Navigation 首页 / Home
C++ 编程 / C++ Programming
系统与高性能 / Systems & Performance
Web 开发 / Web Development
人工智能 / Artificial Intelligence
工业软件 / Industrial Software
其他内容 / Other Topics
C++ 编程 / C++系统与性能 / SystemsWeb 开发 / Web人工智能 / AI工业软件 / Industrial

外观

Sidebar Navigation

← C++ 编程 / C++ Programming

现代 C++ / Modern C++

1. 现代 C++ 总览:不是特性清单,而是表达能力 / Modern C++ Overview

2. 移动、转发与值类别:std::move 为什么不移动 / Move, Forward, and Value Categories

3. 类型推导、初始化与 constexpr / Type Deduction, Initialization, and constexpr

4. 模板、实例化与 SFINAE / Templates, Instantiation, and SFINAE

5. concept 与约束:把模板要求写在接口上 / Concepts and Constraints

6. 异常、RAII 与 noexcept:失败路径也要清理 / Exceptions, RAII, and noexcept

7. lambda、捕获与生命周期:回调不是免费午餐 / Lambdas, Captures, and Lifetime

8. callable wrapper 与 std::invoke:怎样保存和调用行为 / Callable Wrappers and std::invoke

9. 词汇类型与错误建模:让接口自己说话 / Vocabulary Types and Error Modeling

10. 协程与异步抽象:先理解状态机 / Coroutines and Async Abstractions

本页目录

异常、RAII 与 noexcept:失败路径也要清理 / Exceptions, RAII, and noexcept ​

现代 C++ 讨论异常时,不能只问“要不要用异常”。

更重要的是:一旦失败发生,已经获得的资源会不会正确释放?对象会不会保持可用状态?接口有没有承诺不抛?

1. 异常会跳过普通语句 ​

看代码:

cpp
#include <iostream>
#include <stdexcept>

void work() {
    std::cout << "open\n";
    throw std::runtime_error("failed");
    std::cout << "close\n";
}

int main() {
    try {
        work();
    } catch (const std::exception& e) {
        std::cout << "caught: " << e.what() << '\n';
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

close 不会打印。

因为 throw 之后,当前函数不会继续按普通顺序往下执行。

流程是:

text
enter work()
  |
  v
print open
  |
  v
throw exception
  |
  v
leave normal control flow
  |
  v
search matching catch
1
2
3
4
5
6
7
8
9
10
11
12
13

所以如果资源清理只是写在后面的普通语句里,它可能被异常跳过。

这就是异常和资源管理必须一起讨论的原因。

2. 栈展开是什么 ​

异常从抛出点向外传播时,会离开当前函数,继续离开调用者函数,直到找到匹配的 catch。

这个过程叫栈展开。

cpp
void c() {
    throw std::runtime_error("failed");
}

void b() {
    c();
}

void a() {
    b();
}
1
2
3
4
5
6
7
8
9
10
11

调用栈:

text
main
  |
  v
a
  |
  v
b
  |
  v
c throws
1
2
3
4
5
6
7
8
9
10

异常传播时:

text
c cannot handle
  |
  v
leave c
  |
  v
b cannot handle
  |
  v
leave b
  |
  v
a cannot handle
  |
  v
leave a
  |
  v
main catches
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

关键点是:离开每一层函数时,已经构造成功的局部对象会析构。

3. RAII 为什么能覆盖异常路径 ​

看代码:

cpp
#include <iostream>
#include <stdexcept>

struct Guard {
    Guard() {
        std::cout << "acquire\n";
    }

    ~Guard() {
        std::cout << "release\n";
    }
};

void work() {
    Guard guard;
    throw std::runtime_error("failed");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

当异常离开 work():

text
Guard constructed
  |
  v
throw exception
  |
  v
stack unwinding begins
  |
  v
guard destructor runs
  |
  v
resource released
1
2
3
4
5
6
7
8
9
10
11
12
13

这就是 RAII 与异常天然配合的地方。

你不用在每个失败出口手写释放。

只要资源被一个局部对象正确拥有,析构就会覆盖正常返回、提前返回和异常路径。

4. 析构函数不要抛异常 ​

析构函数通常不应该抛异常。

原因是:如果栈展开过程中正在处理一个异常,析构函数又抛出另一个异常,程序很可能调用 std::terminate。

资源清理应该尽量做到:

text
析构函数完成必要清理
不把错误继续抛出
如果必须报告错误,用显式 close/commit 接口提前处理
1
2
3

例如文件写入时,关闭或刷新也可能失败。

如果你需要检查这类错误,可以设计显式操作:

cpp
class Writer {
public:
    void close();  // 可以返回错误或抛异常
    ~Writer();     // 尽量只做兜底清理,不抛
};
1
2
3
4
5

5. 异常安全的三个层次 ​

异常安全不是“完全没有异常”。

它说的是:异常发生后,程序和对象处于什么状态。

常见层次:

text
基本保证
  出错后对象仍然有效,可以析构,可以继续满足基本不变量

强保证
  出错后像操作没有发生过一样

不抛保证
  操作承诺不会抛异常
1
2
3
4
5
6
7
8

例如:

cpp
std::vector<int> values{1, 2, 3};
values.push_back(4);
1
2

如果 push_back 扩容时分配失败,标准库不能让 values 变成内部结构损坏的对象。

至少要保持基本保证。

某些条件下,它还能提供强保证。

6. noexcept 是接口承诺 ​

看代码:

cpp
void f() noexcept {
    throw 1;
}
1
2
3

这不是“编译器会帮你忽略异常”。

如果 f() 真的抛出异常,程序会调用 std::terminate。

所以 noexcept 是承诺:

text
这个函数不会让异常逃出去。
1

它也会影响标准库行为。

例如 vector 扩容时,元素需要搬到新缓冲区。

如果元素的移动构造是 noexcept,vector 更容易选择移动。

如果移动可能抛,为了保持异常安全,vector 可能选择复制。

cpp
struct Item {
    Item() = default;
    Item(Item&&) noexcept = default;
};
1
2
3
4

这里 noexcept 不是装饰。

它是给使用者和标准库的承诺。

7. 什么时候写 noexcept ​

适合写:

text
移动构造和移动赋值确实不会抛
swap 确实不会抛
析构函数
底层释放函数
性能敏感且语义上不允许失败的操作
1
2
3
4
5

不适合乱写:

text
函数内部会分配内存
函数内部会调用可能抛异常的用户代码
函数内部错误需要向上报告
只是为了“看起来现代”
1
2
3
4

写 noexcept 前要问:

text
如果这里真的失败,我能不能在函数内部处理完?
如果不能,是否应该允许异常传播?
1
2

8. 本篇总结 ​

异常和 RAII 的关系:

text
异常改变控制流
  |
  v
普通清理语句可能被跳过
  |
  v
栈展开会析构局部对象
  |
  v
RAII 把清理放进析构
  |
  v
失败路径也能释放资源
1
2
3
4
5
6
7
8
9
10
11
12
13

noexcept 的关系:

text
noexcept
  不是建议
  是承诺
  违反后会 terminate
  会影响标准库选择移动还是复制
1
2
3
4
5

写现代 C++ 资源类型时,要同时考虑:

text
正常路径
提前返回
异常路径
析构是否抛
移动是否 noexcept
对象失败后是否仍然有效
1
2
3
4
5
6

最后更新于:

Pager
上一篇5. concept 与约束:把模板要求写在接口上 / Concepts and Constraints
下一篇7. lambda、捕获与生命周期:回调不是免费午餐 / Lambdas, Captures, and Lifetime

持续记录,持续成长

Copyright © Tidenflow