现代 C++ 实战(08):错误处理策略
C++ 没有统一的「错误处理方式」——return -1、throw、std::optional、std::expected 各有战场。选错方式要么性能吃亏,要么错误被静默忽略,要么 API 语义含糊。
这一篇用同一个「解析配置文件」场景,对比 四种主流错误处理策略,并给出选型指南。对应 demo:ref/cpp_demo/basics/error_handling_demo/。
这是「现代 C++ 实战」系列的第 8 篇。建议先读 第 07 篇:C++17 工具箱。
一、为什么错误处理值得单独讲?
现代 C++ 项目里,错误处理往往比算法更影响可维护性:
- 调用方会不会忘记检查返回值?
- 失败时资源会不会泄漏(文件句柄、锁、内存)?
- API 语义是否清晰:「找不到」和「出错了」是一回事吗?
demo 用解析 host=...、port=... 的配置文件贯穿全文——同一功能,四种写法。
二、C 风格错误码:return -1 + 输出参数
1 | enum class CErrorCode { SUCCESS = 0, FILE_NOT_FOUND = -1, INVALID_FORMAT = -2 }; |
| 优点 | 缺点 |
|---|---|
| 零异常开销,适合底层 / 热路径 | 容易忘记检查返回值 |
| 与 C API、系统调用一致 | 返回值被错误码占用,结果要走输出参数 |
| 简单直观 | 错误码含义靠文档,信息量少 |
适用:系统调用封装、嵌入式、性能极度敏感且错误路径极少的代码。
三、C++ 异常:throw + try/catch + RAII
1 | Config parse_config(const std::string& path) { |
RAII 是异常安全的基石:ifstream、智能指针、lock_guard 在栈上析构时自动释放——即使中途 throw 也不会泄漏。
| 异常安全等级 | 含义 |
|---|---|
| 基本保证 | 不泄漏,对象处于有效状态 |
| 强保证 | 失败时状态不变(如 copy-and-swap) |
| nothrow | 绝不抛异常 |
适用:业务逻辑、构造函数失败、错误路径复杂且需要丰富错误信息的场景。
注意:不要用异常做正常控制流(如循环退出);析构函数默认 noexcept,析构里抛异常会 std::terminate。
四、std::optional:「可能没有值」,但不是错误
1 | std::optional<Config> find_config(const std::string& name) { |
| 场景 | 用 optional |
|---|---|
| map 查找、缓存命中 | ✅ 「没有」是正常结果 |
| 解析失败、文件不存在 | ❌ 应使用 expected 或异常 |
optional 表达的是可空性,不是错误语义——别把「磁盘坏了」和「键不存在」混成一个 nullopt。
五、std::expected<T, E>(C++23):值或错误,类型安全
Rust 的 Result 在 C++23 里的对应物:
1 | enum class ParseError { FileNotFound, InvalidFormat, MissingKey }; |
| 对比 | optional | expected |
|---|---|---|
| 语义 | 可能有值 | 成功值 或 错误 |
| 错误信息 | 无(只有空) | 携带 E 类型 |
| 性能 | 轻量 | 轻量(无异常展开) |
适用:库 API、需要明确错误类型、又不想用异常的场景。编译器需 C++23(__cplusplus >= 202302L)。
六、std::error_code:与系统错误对接
操作系统和 Boost.Asio 等库常用 error_code 而非异常:
1 | std::error_code ec; |
error_code + error_category 把平台相关错误码统一成可比较、可传递的对象——适合 I/O、网络底层。
七、选型指南
| 方式 | 何时用 |
|---|---|
| 错误码 | C 互操作、系统调用、热路径、简单二元成败 |
| 异常 | 构造函数失败、深层调用栈、错误信息丰富、业务层 |
| optional | 「找不到 / 没有」是正常结果,不是 failure |
| expected | 明确的成功/失败联合类型,库 API,无异常策略 |
| error_code | 文件/网络等系统级错误,与 POSIX/Win32 对接 |
决策顺序:
- 「没有值」算正常吗?→ optional
- 失败需要携带类型化错误且不想抛异常?→ expected
- 底层 / C API / 性能关键?→ 错误码 或 error_code
- 其余业务逻辑 → 异常 + RAII
八、noexcept 与性能直觉
- 异常正常路径几乎零开销(零成本抽象的「零」指成功路径)
- 异常抛出路径昂贵(栈展开)——适合「真异常」、低频失败
- 标记
noexcept帮助编译器优化移动操作;违反noexcept会terminate
不要为了「性能」把所有函数改成错误码——在错误极少发生时,异常往往更清晰;在错误是常态时(如解析器逐 token),expected 或错误码更合适。
九、demo 导览
ref/cpp_demo/basics/error_handling_demo/ 对同一配置解析场景演示五种方式(含 error_code):
1 | cd ref/cpp_demo/basics/error_handling_demo |
建议对照输出,看每种方式调用方必须写多少检查代码。
十、小结
| 概念 | 要点 |
|---|---|
| 错误码 | 简单、快,易忽略检查 |
| 异常 | RAII 保安全,适合罕见失败 |
| optional | 可空 ≠ 出错 |
| expected | C++23,值 | 错误,类型明确 |
| error_code | 系统 / I/O 层标准做法 |
现代 C++ 实战系列第 8 篇完。下一篇 C++20 格式化与编译期计算——
std::format与constexpr进阶。
系列导航
| 篇号 | 标题 | 状态 |
|---|---|---|
| 07 | C++17 工具箱 | ✅ |
| 08 | 错误处理策略(本篇) | ✅ |
| 09 | C++20 格式化与编译期计算 | 下一篇 |
完整大纲见工作区 docs/CPP_SERIES_OUTLINE.md。










