std::expected 不能替代 try/catch,它仅替代错误码+手动检查;不触发栈展开,显式返回成功值或错误对象,适用于预期会失败的场景(如I/O、解析),而非真正意外错误。

std::expected 不能替代 try/catch,它只替代「返回错误码 + 手动检查」
很多人以为 std::expected 是异常的“平替”,其实不是:它不触发栈展开,不改变控制流,也不捕获异常。它的定位是让函数**显式返回成功值或错误对象**,把错误处理从隐式(抛异常)变成显式(检查 has_value() 或用 and_then 链式传递)。适合 I/O、解析、配置加载等「预期会失败」的场景,而不是空指针解引用这类真正意外的错误。
常见错误现象:std::expected<int std::string> f() { throw std::runtime_error("oops"); }</int> —— 这不会自动转成 std::unexpected,而是直接崩溃。必须手动构造 std::unexpected 或用 return std::unexpected("oops")。
- 函数签名要提前决定是否用
std::expected:一旦返回std::expected,调用方就必须处理两种状态,没法“假装没看见错误” - 错误类型必须可复制/可移动,且不能是
void;std::expected<void e></void>合法,但std::expected<t void></t>不合法 - 和
std::optional不同,std::expected的错误分支是第一类公民,不是“缺省值”的补丁
用 and_then 和 or_else 实现零开销链式错误传播
and_then 只在有值时调用回调,回调必须返回另一个 std::expected;or_else 只在出错时调用,用于错误恢复或转换。它们不分配内存,也不抛异常,纯函数式风格。
使用场景:读文件 → 解析 JSON → 提取字段。每一步都可能失败,但你不想写三层 if (!res.has_value()) return res;。
立即学习“C++免费学习笔记(深入)”;
示例:
auto load_config = [](std::string_view path) -> std::expected<std::string, std::string> {
std::ifstream f(path.data());
if (!f.is_open()) return std::unexpected("open failed");
return std::string{std::istreambuf_iterator(f), {}};
};
auto parse_json = [](std::string s) -> std::expected<nlohmann::json, std::string> {
try { return nlohmann::json::parse(s); }
catch (const nlohmann::json::exception& e) { return std::unexpected(e.what()); }
};
auto get_port = [](nlohmann::json j) -> std::expected<int, std::string> {
if (j.contains("port") && j["port"].is_number_integer())
return j["port"].get<int>();
return std::unexpected("missing or invalid port");
};
auto port = load_config("/etc/app.json")
.and_then(parse_json)
.and_then(get_port);
-
and_then回调里如果抛异常,程序直接终止——它不捕获,所以务必确保回调自身不抛 -
or_else返回std::expected才能继续链式调用;若只想记录日志并转成新错误,得写return std::unexpected(new_msg) - 编译器对链式调用优化很好,但嵌套过深(>5 层)可能影响调试体验——错误位置指向最后一层,而非源头
和传统错误码对比:别忽略移动语义和值类别陷阱
用 int 错误码 + errno 风格,容易误读成功值为错误码(比如返回 -1 表示 EOF,但业务逻辑也用 -1 表示未设置)。std::expected 强制区分,但代价是:每次移动都涉及判别字段(has_value_)和对齐填充,比裸 int 大 2–4 字节。
性能影响:小对象(如 std::expected<int std::error_code></int>)通常内联无开销;大错误类型(如含堆内存的 std::string)要注意避免不必要的拷贝。
- 别直接
return e.value()从std::expected中取值——先用e.has_value()或e.value_or(0),否则触发std::terminate -
e.value()是左值,e.value_or(x)返回右值;若T移动代价高,优先用std::move(e).value()避免二次拷贝 - 兼容性注意:MSVC 19.35+、GCC 12.2+、Clang 16+ 才完整支持 C++23 的
std::expected;旧版本需用absl::StatusOr或tl::expected替代
什么时候不该用 std::expected?
当错误发生意味着程序处于不可恢复的非法状态时,比如内存耗尽、硬件故障、断言失败——这些该用异常或 std::terminate,而不是塞进 std::expected 里假装还能“继续执行”。还有就是高频小函数(如数学库的 sqrt),加一层 std::expected 会破坏内联和向量化。
最容易被忽略的一点:std::expected 的错误类型必须明确表达“为什么失败”,而不是笼统的 int。用 std::error_code 比 int 好,但自定义枚举(如 enum class parse_err { missing_comma, invalid_number })配合 std::expected<t parse_err></t>,才能让调用方做精确分支处理,而不是靠字符串匹配。










