std::expected 不捕获异常,仅显式携带成功值或错误值;必须手动 try/catch 后构造 std::unexpected,否则抛异常直接终止程序。

std::expected 不是用来“处理业务异常”的,它根本不捕获异常;它只负责显式携带成功值或错误值。想用它替代 try/catch 处理 std::runtime_error 或 std::ios_base::failure,必须手动封装、主动捕获、显式构造——否则代码直接崩溃。
std::expected 不能自动转译抛出的异常
常见错误是写这样的函数:
std::expected<int, std::string> risky_op() {
throw std::runtime_error("oops"); // ❌ 不会变成 std::unexpected
}
这不会自动包装成 std::unexpected("oops"),而是直接终止程序。C++23 的 std::expected 是纯值语义类型,不介入异常机制。
- 所有异常必须在函数体内用
try/catch捕获,再调用std::unexpected(...)构造失败态 - 不能依赖编译器或标准库“升级”老接口:比如
std::ifstream构造函数永远不会返回std::expected - 若底层函数已设
exceptions(failbit | badbit),它仍会抛异常——你得先关掉它,再手动查rdstate()
相比 std::optional,std::expected 明确表达错误语义
std::optional<t></t> 只能表示“有或没有”,无法说明“为什么没有”;std::expected<t e></t> 强制你定义错误类型 E,让业务意图可读、可测试、可传播。
立即学习“C++免费学习笔记(深入)”;
- 例如:用
enum class config_errc { missing_file, invalid_yaml, permission_denied }替代裸std::string,避免拼写错误和模糊日志 -
std::optional<int></int>对除零返回nullopt,但调用方无法区分是“输入为0”还是“内存分配失败”;std::expected<int std::errc></int>能精确传递std::errc::invalid_argument -
and_then链式调用时,std::optional一旦为空就静默终止链,而std::expected的or_else允许你对每种错误做差异化恢复
封装文件读取时,错误类型选 std::error_code 还是自定义枚举?
优先用 std::error_code,但必须配对正确 category。文件系统错误属于 std::system_category(),不是 std::generic_category()。
- 错误写法:
std::error_code{errno, std::generic_category()}—— 导致.message()返回空或乱码 - 正确写法:
std::error_code{errno, std::system_category()},或用 C++23 的std::filesystem::status_error等专用类型 - 若需业务语义(如 “配置字段 timeout 缺失”),应组合使用:外层
std::expected<config config_errc></config>,内层 I/O 错误用std::error_code封装到底层 - 避免把
std::string当错误类型——它不可比、不可序列化、无法做switch,且移动开销大
and_then 链式调用中最容易忽略的生命周期陷阱
and_then 回调里若捕获局部变量引用,或返回临时对象的引用,会导致悬垂指针或未定义行为。尤其当错误类型含 RAII 成员(如 std::string)时更危险。
- 错误示例:
.and_then([&buf](auto& s) { return parse_json(s); })——buf可能在回调执行前被析构 - 正确做法:按值捕获,或确保所有被捕获对象生命周期覆盖整个链;对大对象考虑
std::shared_ptr或 move 语义 - 别在
and_then里调用可能抛异常的函数(如nlohmann::json::parse()),必须包一层try/catch并转为std::unexpected - 链中任意一步返回
std::expected<void e></void>是合法的,但后续and_then必须接受std::expected<u e></u>,不能假设值类型不变
最常被跳过的一步:检查 std::expected::has_value() 前,先确认你没在构造时传入不可移动的对象(比如裸 std::ifstream)——它既不能拷贝也不能移动,会导致编译失败,而不是运行时报错。


















