1. 为什么我们需要 std::expected
在C++的底层开发中,错误处理一直是个棘手的问题。传统方式无非两种:返回错误码或者抛出异常。但这两者都有明显的缺陷。错误码要求调用者必须显式检查,而异常又太重,特别是在性能敏感的领域。
我最近在开发一个网络协议栈时深有体会。协议解析涉及多层嵌套调用:从字节流解析到数据包,再到应用层消息。每一层都可能出错,而错误需要跨多层传递。用错误码会让代码充满if-else,而用异常又担心性能损耗。
这就是std::expected的价值所在。它把"可能出错"这个事实直接编码到类型系统中,强制调用者处理,同时又没有运行时开销。你可以把它看作是一个加强版的std::optional,不仅能表示"有值/无值",还能携带错误详情。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. std::expected 的核心设计
2.1 代数数据类型(ADT)的C++实现
std::expected本质上是个标签联合体(tagged union),可以持有两种值之一:
- 预期的成功值(T类型)
- 非预期的错误值(E类型)
这种设计借鉴了函数式编程中的Either类型。与C++17的std::variant类似,但语义更明确——其中一个分支明确表示"错误"。
cpp复制std::expected<Packet, ParseError> parse_packet(Bytes data);
这个声明一眼就能看出:函数要么返回有效的Packet,要么返回ParseError。调用方必须显式处理这两种情况。
2.2 与异常处理的性能对比
在底层代码中,异常处理的成本主要来自三方面:
- 栈展开时的析构调用
- 异常对象的构造和拷贝
- 编译器生成的额外代码
我们实测过一个解析器的性能:
- 异常版本:错误路径耗时约1200ns
- std::expected版本:错误路径仅约15ns
- 成功路径两者差异可以忽略
差异主要来自异常需要维护调用栈信息,而std::expected只是简单的值传递。
3. 链式调用中的实践技巧
3.1 错误传播的Monadic接口
std::expected支持类似optional的monadic操作,让链式调用更优雅:
cpp复制std::expected<Respo
