std::expected 比 throw 更适合状态机错误传递,因其将错误作为值显式携带,支持链式调用、编译期检查、可调试性及异步友好,避免异常破坏控制流与中间状态丢失。

为什么 std::expected 比 throw 更适合状态机错误传递
状态机在流转过程中,每个状态转移都可能失败(如输入非法、资源不可用、前置条件不满足),但抛异常会破坏控制流可预测性,且难以在栈展开前捕获中间状态。而 std::expected<t e></t>(C++23)或 absl::StatusOr 类型能将错误作为值显式携带,让状态转移函数返回 std::expected<nextstate error></nextstate>,天然契合“单步执行 → 检查结果 → 决定下跳”的链式逻辑。
关键点在于:错误不是被“抛出”,而是被“传递”和“组合”。你不需要 try/catch 嵌套,只需对 expected 调用 and_then 或手动 if (auto r = step1(); r.has_value()) { ... }。
-
throw会让调试器跳过中间状态快照,而expected的值始终可 inspect - 异步或协程状态机中,
exception_ptr传递成本高,expected是 trivially copyable(若E满足) - 编译期可强制检查是否处理了所有错误分支(配合
[[nodiscard]]和 clang-tidy 检查)
and_then 如何安全串联多个状态转移函数
C++23 的 std::expected 尚未内置 and_then,需自行实现或用第三方(如 tl::expected)。它的作用是:仅当上一步成功(has_value())时才调用下一步,否则短路返回原错误——这正是状态机“失败即终止、不继续推进”的语义。
示例片段:
template<typename t typename e f>
auto and_then(std::expected<t e> const& e, F&& f) {
if (e.has_value()) return std::forward<f>(f)(e.value());
else return std::expected<typename std::invoke_result_t t>::value_type, E>{unexpect, e.error()};
}
</typename></f></t></typename>
- 注意返回类型推导:
f返回的也必须是std::expected,否则链会断掉 - 不要在
f中 throw,否则破坏 monadic 不变式;应统一转为std::unexpected(e) - 若某步需访问前序状态数据(如“验证 token 后再加载用户”),把状态打包进
T(例如struct State { Token t; User* u; };),避免闭包捕获导致生命周期问题
如何封装错误使状态机日志与恢复策略可配置
直接用 std::string 或 enum class ErrorCode 做 E 类型太单薄。推荐定义分层错误类型:
struct StateError {
enum class Code { InvalidInput, Timeout, PermissionDenied };
Code code;
std::string context; // 当前状态名、输入摘要等
std::chrono::steady_clock::time_point timestamp;
};
这样做的好处:
- 日志系统可按
code分类告警,context提供现场快照,无需额外打点 - 恢复策略可基于
code匹配:如Timeout自动重试,PermissionDenied跳转到授权页 - 序列化导出时,
context可含 JSON 片段(如"{\"user_id\":123,\"action\":\"pay\"}"),便于后端归因
别把错误信息拼接进 std::string —— 那会让 pattern matching 和静态分析失效。
常见陷阱:移动语义与 expected 生命周期错位
当状态转移函数返回临时 std::expected,而你又在 lambda 中引用其 value(),容易触发悬垂引用:
auto bad = [&]() {
return state_machine.step_a().and_then([&](auto s) {
return step_b(s); // ❌ s 是临时对象的引用!
});
};
- 正确做法:在
and_then回调内消费s,或显式 move:step_b(std::move(s))(如果s可移动) - 若
T是大对象(如std::vector<byte></byte>),考虑用std::shared_ptr<t></t>包装,避免重复拷贝 - Clang 15+ 可启用
-Wdangling-gsl捕获此类问题;GCC 尚无等效检查
monadic 链不是语法糖,它是把控制流责任从运行时转移到类型系统——没处理好移动语义,类型安全就塌了一角。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











