std::expected 适合替代文件打开、json解析、网络响应解析等具有明确成功路径和可枚举带上下文错误的错误码模式,而不适用于状态机控制流信号或void函数错误码。

std::expected 适合替代哪些错误码模式
直接用 std::expected 替换所有 int 错误码是危险的——它只适合「成功路径明确、失败原因可枚举且需携带上下文」的场景。比如文件打开、JSON 解析、网络请求响应解析,这类操作天然有「值 or 错误」二元结构;但像状态机流转中返回 STATE_INVALID / STATE_BUSY 这类控制流信号,强行套 std::expected 反而模糊意图。
常见误用:把 void 函数的错误码(如 int init();)改成 std::expected<void err></void>。这没带来额外信息,还引入了不必要的模板开销和移动语义约束。
构造与解包时必须处理 error 分支
std::expected 不会自动抛异常,也不隐式转换。如果只写 auto res = parse_json(str); if (res) { use(*res); },漏掉 !res 分支,在 debug 模式下可能触发断言(取决于实现),release 下则静默忽略错误——比传统错误码更难调试。
推荐写法:
auto res = parse_json(str);
if (res) {
process(*res);
} else {
log_error(res.error()); // 必须显式处理 error
return std::unexpected(res.error());
}
注意:res.error() 返回的是 Err 类型的 const 引用,不能直接赋值给非 const 变量;若需修改,得先拷贝。
error 类型选 enum class 还是自定义 struct
用 enum class 简洁但丢失上下文:比如解析失败只知道是 ERR_MISSING_COMMA,却不知错在哪一行。此时应选带字段的 struct:
struct ParseError {
int line;
std::string message;
// 必须提供默认构造函数,否则 std::expected 编译不过
ParseError() : line(0), message("") {}
};
关键约束:
-
Err类型必须可默认构造(std::expected内部需要初始化 error 存储) - 若
Err含非 trivial 析构函数(如含std::string),则std::expected对象不可noexcept移动 - 避免用
std::error_code直接塞进去——它本身不满足三法则要求,需包装一层
与现有 C API 或 errno 风格函数桥接
不能直接把 open() 的返回值塞进 std::expected<int int></int>——errno 是全局状态,多线程下竞态风险高。正确做法是立即捕获 errno:
int fd = open(path.c_str(), flags);
if (fd == -1) {
return std::unexpected(errno); // 立即读取,别等后续语句
}
return fd;
更安全的方式是封装系统调用为独立函数,并在函数内完成 errno 捕获。不要试图在调用链上游统一处理 errno——那已经晚了。
另一个坑:C API 返回指针(如 fopen),习惯性写 std::expected<file int></file>。但 FILE* 为空时,error 分支反而成了「正常失败」,逻辑反直觉。此时更适合用 std::expected<:unique_ptr>, Err></:unique_ptr>,让 value 分支始终代表有效资源。
std::expected,而是判断某个函数是否「值得」重构——值和错误是否天然互斥?错误是否需要携带数据?调用方是否真会检查 error 而不是只看 value?这些问题没想清,加了 std::expected 只会让代码更重。 C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











