std::expected是c++23引入的用于显式处理可预期错误的类型,适用于文件打开、网络请求、配置解析等调用方必须处理失败的场景,而非替代异常或std::optional。

std::expected 是什么,什么时候该用它
std::expected 是 C++23 引入的标准化错误传播类型,本质是 std::variant<t e></t> 的语义封装:成功时存 T,失败时存 E。它不是替代 throw 的万能方案,而是为「预期会失败、且调用方必须显式处理」的场景设计的——比如文件打开、网络请求、解析 JSON 字符串。
常见误用是把它当“不抛异常的 std::optional”:如果错误本该中断流程(如内存分配失败),硬塞进 std::expected 反而模糊语义;如果错误根本不会发生(比如访问已验证存在的容器元素),用它纯属冗余。
适用典型场景包括:
- 系统调用封装(
open()、getaddrinfo()) - 配置解析(INI/JSON/YAML 解析失败返回
std::expected<config parseerror></config>) - 组合式管道操作(
parse() -> validate() -> transform()链式传递错误)
如何正确构造和检查 std::expected
构造 std::expected 有两类方式:成功值用花括号初始化,错误值用 std::unexpected 包装:
std::expected<int std::string> parse_int(const char* s) {
char* end;
long val = std::strtol(s, &end, 10);
if (*s == '\0' || *end != '\0') {
return std::unexpected(std::string("invalid integer: ") + s);
}
return val; // 自动转为 expected<int string>
}
</int></int>
检查时避免直接访问 .value()——它在 error 状态下会抛 std::bad_expected_access。正确做法是:
- 用
.has_value()判断状态 - 用
.value_or(default_val)提供兜底值(仅当E可默认构造) - 用
.and_then()或.or_else()做链式转换(C++23 支持,注意编译器需启用/std:c++23或-std=c++23)
一个易错点:std::expected<t e></t> 的 E 类型不能是引用或数组——否则编译失败,常见于误写 std::expected<int const char></int>。
链式调用中怎么传递错误而不打断逻辑
std::expected 的核心价值在组合:每个函数返回 std::expected,上游错误自动透传,无需层层 if (!ok) return ...。
auto read_config() -> std::expected<config std::string> { ... }
auto validate(const Config&) -> std::expected<config validationerror> { ... }
auto load_data(const Config&) -> std::expected<data ioerror> { ... }
<p>// 组合写法(C++23)
auto result = read_config()
.and_then(validate)
.and_then(load_data);</p></data></config></config>
注意三点:
-
and_then回调函数必须返回std::expected(哪怕类型不同),否则编译报错 - 错误类型不兼容时(如
std::stringvsValidationError),链会中断——这时得用.or_else()显式转换错误类型 - 若中间某步想忽略错误并转成默认值,可用
.value_or(...),但会丢失错误上下文,慎用
和 std::optional、异常、errno 对比时的实际取舍
std::optional 表达“可能无值”,但不携带错误原因;std::expected 明确区分“值缺失”和“失败原因”。例如解析 URL:空字符串用 std::optional<url></url> 合理,但语法错误必须返回 std::expected<url parseerror></url> 才能让调用方知道是哪部分出错。
异常适合“意外、不可恢复”的错误(如 std::bad_alloc),而 std::expected 适合“可预测、可恢复”的失败路径。混用两者容易混乱:不要在 std::expected 函数里 throw,也不要在异常驱动的代码里突然引入 std::expected。
至于 errno:它全局、非线程安全、语义模糊(同一个值在不同系统含义不同)。用 std::expected<int std::errc></int> 封装系统调用更清晰,但要注意 std::errc 是枚举,需显式转成 std::error_code 才能打印描述。
实际项目中最大的坑是:团队成员对错误分类没共识,有人把所有错误都塞进 std::expected,有人坚持全用异常,结果接口契约混乱。统一约定比语法技巧重要得多。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











