std::expected 不能直接替代异常,它适用于可预期且需主动处理的失败路径,如字符串解析、配置文件读取等;异常则用于意外但必须响应的严重错误。

std::expected 不能直接替代异常处理
它和异常是不同层级的错误处理机制:异常用于“意外但必须响应”的错误(比如文件系统崩溃),std::expected 用于“可预期且调用方应主动检查”的失败路径(比如解析 JSON 失败、查找 map 中不存在的 key)。强行用 std::expected 替代所有异常,会导致错误传播链冗长、关键错误被静默吞掉。
什么时候该用 std::expected
典型场景是函数契约明确、失败原因有限、且调用方有能力/责任做差异化处理:
- 字符串解析:
parse_int(std::string_view)返回std::expected<int std::errc></int> - I/O 封装层(非底层系统调用):
read_config_file()返回std::expected<config configerror></config> - 算法预检失败:
validate_input(data)返回std::expected<void validationerror></void>
注意:E 类型应轻量、可拷贝,避免用 std::string 或动态分配对象;推荐用枚举类(如 enum class ParseErr)或 std::errc。
std::expected 的常见误用和坑
很多人一上来就套模板,结果写出难以维护的代码:
立即学习“C++免费学习笔记(深入)”;
- 嵌套太深:不要写
std::expected<:expected e1>, E2></:expected>,这说明接口职责不清,应合并错误类型或拆分函数 - 忽略值检查:直接调用
e.value()而不先判断e.has_value(),运行时抛std::bad_expected_access—— 这反而引入了隐式异常 - 与异常混用混乱:同一个模块里一部分用
throw,一部分用std::expected,导致错误处理逻辑分裂 - 忘记移动语义:返回大对象时没用
std::move,触发不必要的拷贝(尤其T不是 trivially copyable 时)
一个安全、简洁的使用模式
推荐统一用 if (auto r = func(); r) { use(r.value()); } 模式,配合自定义错误处理辅助函数:
auto r = parse_json(input);
if (!r) {
log_error("JSON parse failed: {}", r.error());
return std::nullopt;
}
return process(*r);
更进一步,可用 C++23 的 std::expected::and_then 链式传递成功值,但注意:一旦某个环节返回 std::unexpected,后续 and_then 不执行,整个链自动短路。这点和 std::optional 的 transform 行为一致,但比手写 if 嵌套更清晰。
真正难的不是怎么写 std::expected,而是界定哪些错误属于“业务可恢复”,哪些属于“程序已不可信”——后者仍得靠异常或 std::terminate。










