现代 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=...、ssl=... 的 config.txt 贯穿全文——同一功能,五种写法(C 错误码、异常、error_code、optional、expected),下文前四节对应四种主流策略,error_code 放在第六节单独讲。
二、C 风格错误码:return -1 + 输出参数
1 | enum class CErrorCode { |
| 优点 | 缺点 |
|---|---|
| 零异常开销,适合底层 / 热路径 | 容易忘记检查返回值 |
| 与 C API、系统调用一致 | 返回值被错误码占用,结果要走输出参数 |
| 简单直观 | 错误码含义靠文档,信息量少 |
适用:系统调用封装、嵌入式、性能极度敏感且错误路径极少的代码。
三、C++ 异常:throw + try/catch + RAII
1 | class ConfigException : public std::runtime_error { |
RAII 是异常安全的基石:ifstream、智能指针、lock_guard 在栈上析构时自动释放——即使中途 throw 也不会泄漏。
| 异常安全等级 | 含义 |
|---|---|
| 基本保证 | 不泄漏,对象处于有效状态 |
| 强保证 | 失败时状态不变(如 copy-and-swap) |
| nothrow | 绝不抛异常 |
适用:业务逻辑、构造函数失败、错误路径复杂且需要丰富错误信息的场景。
demo 还演示了 noexcept:validate_config(const Config&) noexcept 发现端口越界时只打印警告,不能 throw。
注意:不要用异常做正常控制流(如循环退出);析构函数默认 noexcept,析构里抛异常会 std::terminate。
四、std::optional:「可能没有值」,但不是错误
1 | std::optional<Config> find_config(const std::string& name) { |
| 场景 | 用 optional |
|---|---|
| map 查找、缓存命中 | ✅ 「没有」是正常结果 |
| 解析失败、文件不存在 | ❌ 应使用 expected 或异常 |
optional 表达的是可空性,不是错误语义——别把「磁盘坏了」和「键不存在」混成一个 nullopt。demo 里的 parse_config_optional 为了横向对比,把「文件不存在 / 格式错误 / 缺键」统统返回 std::nullopt,调用方只能打印「未找到配置」——正好说明这种用法丢失了多少信息。
五、std::expected<T, E>(C++23):值或错误,类型安全
Rust 的 Result 在 C++23 里的对应物:
1 | struct ConfigError { |
| 对比 | optional | expected |
|---|---|---|
| 语义 | 可能有值 | 成功值 或 错误 |
| 错误信息 | 无(只有空) | 携带 E 类型 |
| 性能 | 轻量 | 轻量(无异常展开) |
适用:库 API、需要明确错误类型、又不想用异常的场景。编译器需 C++23(__cplusplus >= 202302L);demo 用这个条件定义 HAS_EXPECTED,不满足时 demo_expected() 只打印当前 __cplusplus 值。
六、std::error_code:与系统错误对接
操作系统和 Boost.Asio 等库常用 error_code 而非异常:
1 | std::error_code ec; |
error_code + error_category 把平台相关错误码统一成可比较、可传递的对象——适合 I/O、网络底层。
demo 更进一步,为配置解析自定义了错误类别:
1 | enum class ConfigErrc { success = 0, file_not_found, invalid_format, missing_key }; |
失败时 ec.message() 给出可读信息,ec.category().name() 为 "config";需要时还能 throw std::system_error(ec) 转成异常。
七、选型指南
| 方式 | 何时用 |
|---|---|
| 错误码 | 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)。main 先在当前目录写出 config.txt(host=localhost、port=8080、ssl=true),再依次调用 demo_c_style()、demo_exception()、demo_error_code()、demo_optional()、demo_expected(),最后打印一张对比表:
1 | cd ref/cpp_demo/basics/error_handling_demo |
demo 的 CMake 是 set(CMAKE_CXX_STANDARD 23),需要 GCC 12+ / Clang 16+ / Apple Clang 15+ 这类带 <expected> 的标准库;默认构建即可看到第 5 段的实际输出:
1 | 解析成功: host=localhost, port=8080, ssl=1 |
第二行来自 and_then 链:demo 用 auto validated = result.and_then(...) 接住返回值再判断,验证失败时打印「配置验证失败」。如果编译器不支持 std::expected,第 5 段会退化为一句提示,其余 4 段不受影响。
建议对照输出,看每种方式调用方必须写多少检查代码。
十、小结
| 概念 | 要点 |
|---|---|
| 错误码 | 简单、快,易忽略检查 |
| 异常 | RAII 保安全,适合罕见失败 |
| optional | 可空 ≠ 出错 |
| expected | C++23,值 | 错误,类型明确 |
| error_code | 系统 / I/O 层标准做法 |
现代 C++ 实战系列第 8 篇完。下一篇 C++20 格式化与编译期计算——
std::format与constexpr进阶。
系列导航
| 篇号 | 标题 | 状态 |
|---|---|---|
| 07 | C++17 工具箱 | ✅ |
| 08 | 错误处理策略(本篇) | ✅ |
| 09 | C++20 格式化与编译期计算 | ✅ |











