应使用 std::expected 而非 std::optional 承担错误语义,因前者明确区分成功与失败并携带可分类的错误原因,后者仅表示值是否存在;std::expected 适用于有明确失败路径的场景,如文件读取或JSON解析,而 std::optional 适合表示业务上天然可能不存在的值。

别用 std::optional 承担错误语义——它不是为失败设计的,强行塞进去只会让调用方猜错因。
std::expected 和 std::optional 的根本区别在哪
核心就一条:std::optional 只回答“有没有值”,std::expected 回答“成功了没?如果没,为什么?”
常见错误现象:std::optional<int> 被用来表示除零失败,结果调用方看到 std::nullopt,只能靠文档或注释猜是参数非法、内存不足还是磁盘满。
-
std::optional的std::nullopt是空值语义,不带任何上下文 -
std::expected的std::unexpected(E)是错误语义,E必须能说明原因(如std::error_code、std::string或自定义枚举) -
std::expected要求E类型可复制/可移动,且不能是void(否则该用std::optional) -
std::optional支持隐式转换到bool,std::expected不支持——必须显式调用.has_value()或用结构化绑定判断
什么时候该选 std::expected 而不是 std::optional
看函数是否“本意是成功,但有明确、可分类的失败路径”。
立即学习“C++免费学习笔记(深入)”;
组合式C++代码评审方案,融合静态分析、AI推理、多轮迭代评审和C++专项检查,适用于PR审查、增量代码审查、全项目评审和代码质量评分,触发词包括review cpp、cpp代码评审、C++review、代码审查。
典型场景包括:文件读取、JSON 解析、系统调用封装、配置加载、网络响应解析。
- 用
std::optional<User>适合find_user_by_id(int id)——用户可能真不存在,这不是错误,是业务常态 - 用
std::expected<User, std::error_code>适合load_user_from_file(const std::string& path)——文件打不开、格式损坏、权限拒绝,都是需区分处理的具体错误 - 如果错误类型需要携带字符串上下文(比如 “JSON parse error at line 42: unexpected token '}'”),
std::expected<T, std::string>比std::error_code更直接
std::expected 的 and_then 链式调用怎么写才不翻车
and_then 是函数式错误传播的核心,但它只在每个环节都返回 std::expected 时才真正可靠。
常见误用:中间某步返回 std::optional 或裸值,导致编译失败或逻辑断裂。
- 所有
and_then的回调必须返回std::expected<U, E>(其中U可与原T不同,但E类型必须一致) - 若某步可能失败但无错误细节,优先封装为
std::expected<T, std::monostate>,而非降级为std::optional - 不要在
and_then里 throw 异常——这会破坏预期的错误流,也违背std::expected的设计初衷
std::expected 错误类型选 std::error_code 还是 std::string?
这不是性能问题,而是接口契约问题:错误值要能被调用方机械识别并分支处理。
- 用
std::error_code(配合std::errc或自定义enum class)适合需要程序化响应的场景,比如重试、降级、日志分级 - 用
std::string适合调试、开发期快速反馈或错误信息需直接透出给终端用户(如 CLI 工具) - 混合使用很危险:比如一个函数返回
std::expected<T, std::error_code>,下游却想用.error() == "timeout"判断——编译不过,语义也错乱
最容易被忽略的一点:错误类型 E 的构造开销会影响 std::expected 的移动和异常安全保证。别把带大堆栈对象或动态分配的类型当 E 用,除非你确认过它的移动构造是 noexcept 且廉价。

















