std::expected不能直接存异常对象,因其错误类型e需可复制/移动且非void、非引用/数组,而std::exception不可复制;应改用std::error_code或自定义轻量错误结构体并手动注入__file__、__line__等上下文。

std::expected 为什么不能直接存异常对象
因为 std::expected<t e></t> 要求错误类型 E 必须是可复制/可移动、非 void、且不能是引用或数组——但更重要的是,它不捕获栈展开上下文。你不能把 std::exception 实例(尤其是带 trace 的自定义异常)直接塞进去当 E,除非你显式拷贝其 what() 或保存 std::error_code + 额外字段。
常见错误现象:std::expected<int std::runtime_error></int> 编译失败,报错类似 ‘std::runtime_error’ is not copyable(C++23 前标准库实现大多禁用不可复制类型)。
- 正确做法:用
std::error_code表示系统级错误,或自定义轻量错误结构体(含enum class code+std::string_view context) - 避免把
std::exception_ptr当E类型——它虽可复制,但无法自动还原原始异常类型和消息,失去上下文语义 - 若真需完整异常信息,应封装为值语义类型:
struct ContextualError { std::error_code ec; std::string context; // 如 "failed to parse config at line 42" };
如何在函数返回中携带文件名、行号等上下文
std::expected 本身不提供调用点信息,必须靠调用方主动注入。最简方式是在构造 E 时传入当前作用域的 __FILE__、__LINE__ 等宏,但要注意字符串生命周期。
- 推荐用
std::string_view存储__FILE__(它是静态字符串字面量),避免拷贝;__LINE__直接存为int - 不要在
std::expected构造里调用std::source_location::current()—— C++20 的std::source_location是运行时获取,开销大且多数编译器未优化,不如宏直接 - 示例:
std::expected<int contextualerror> read_int_from_file(const char* path) { if (!std::ifstream{path}) { return std::unexpected(ContextualError{ std::make_error_code(std::errc::no_such_file_or_directory), std::string_view{"read_int_from_file at "} + std::string_view{__FILE__} + ":" + std::to_string(__LINE__) }); } // ... }</int>
std::expected::and_then 和错误传播的陷阱
and_then 只对成功值调用,但它返回的新 std::expected 的错误类型必须与原类型一致——否则编译失败。这意味着:跨层错误类型不统一时,不能无脑链式调用。
- 如果
f()返回std::expected<t err1></t>,而g()返回std::expected<u err2></u>,则f().and_then(g)会失败,除非Err1 == Err2 - 解决方法:提前统一错误类型,例如所有函数都用
ContextualError;或用transform_error手动转换:auto res = f().transform_error([](auto e) { return ContextualError{e.ec, "wrapped from f: " + std::string(e.context)}; }).and_then(g); - 注意
and_then不会自动提升嵌套的std::expected<:expected>></:expected>—— 这不是 monad,没有 flat_map 语义
与传统 try/catch 混用时的性能与语义边界
std::expected 本质是值语义错误处理,和异常机制正交。混用时最易忽略的是:异常逃逸路径不会自动转成 std::unexpected,反之亦然。
- 不要在
std::expected函数内部 throw——除非你明确要终止当前逻辑并让上层 catch,这破坏了预期的“无异常”契约 - 若底层 API 必须用异常(如
std::stoi),应立即捕获并转为std::unexpected:try { return std::stoi(s); } catch (const std::exception&) { return std::unexpected(ContextualError{ std::errc::invalid_argument, std::string_view{"stoi failed on "} + s }); } - 编译器对
std::expected的优化已较好(GCC 13+/Clang 16+),但若函数声明为noexcept,又内部抛异常,会导致 terminate——这点比裸指针更隐蔽
真正难的不是怎么写 std::expected,而是决定哪些错误该用它承载、哪些仍该走异常——比如 I/O 超时算预期失败(用 expected),内存分配失败算意外崩溃(留 exception)。这个边界一旦划错,后续所有上下文注入都会失焦。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











