std::expected 本身不支持“多个失败路径”的自动分支选择,它只表达“成功 or 单一错误类型”;所谓“多个失败路径”,本质是错误值 E 的内部可区分性问题——需通过枚举、std::variant 或自定义结构体等统一 E 类型来承载不同失败原因,而非嵌套或混用多种 E。

直接说结论:std::expected 本身不支持“多个失败路径”的自动分支选择,它只表达“成功 or 单一错误类型”。所谓“多个失败路径”,本质是错误值 E 的内部可区分性问题——你得靠 E 自身的类型设计(比如枚举、带 tag 的结构体)来承载不同失败原因,而不是靠 std::expected 套多层。
std::expected 的 E 必须能区分多种失败场景
很多人误以为要写 std::expected<:expected std::errc>, std::string></:expected> 来表示“先可能 errc,再可能 string”,这是错的。嵌套 std::expected 不仅增加解包成本,还破坏了错误传播的线性语义。
- 正确做法是统一错误类型
E,让它能覆盖所有可能失败点:用enum class(如ParseError)、std::variant<:errc std::string int></:errc>,或自定义结构体 - 例如读配置文件:打开失败、JSON 解析失败、字段缺失,都应映射到同一个
E枚举中,而非让每个步骤返回不同E - 若混用
std::errc和std::string作错误类型,and_then链会断裂——因为类型不匹配,编译不过
and_then 链中如何保留原始错误并追加上下文
and_then 天然只透传上游错误,不会自动“增强”错误信息。想在失败时知道“哪一步挂了”,不能依赖链式自动记录,而要手动封装。
- 在每步函数入口加日志或构造带位置信息的错误:比如
return std::unexpected(ParseError::JsonParseFailed)比std::errc::invalid_argument更具业务语义 - 避免在 lambda 里裸调
result.error()后再构造新std::unexpected—— 容易丢掉原始error_code的 category 和 value - 若需组合错误(如“打开 config.json 失败:Permission denied”),推荐用
or_else接住错误后做字符串拼接,但注意这会失去std::error_code的可比对性
对接 C API 或系统调用时 errno 怎么映射到多路径错误
C 系统调用失败后,errno 是唯一线索,但它本身不区分“是 open 失败还是 read 失败”。必须在调用点立刻捕获并转换,不能延迟。
立即学习“C++免费学习笔记(深入)”;
- 调
open()后立即检查:若返回 -1,则用std::make_error_condition(static_cast<:errc>(errno))</:errc>转成标准std::errc;若想区分“no such file”和“permission denied”,就直接用errno == ENOENT或errno == EACCES构造不同枚举值 - 不要把
errno存到某个全局变量里再统一处理——中间可能被其他系统调用覆盖 - 自定义
std::error_category时,必须重写default_error_condition,否则error_code{2, my_cat} == std::errc::...判断会失效
最易被忽略的一点:错误类型的生命周期和比较语义必须一致。用 std::string 当 E 很方便,但它不可比对、不可序列化、不能和 std::filesystem::read_bytes 返回值直连;而 std::errc 虽轻量,却无法表达业务专属错误(比如“license expired”)。选哪个,取决于你是否需要跨模块传递错误、是否要写入日志做聚合分析。


















