可重入FSM要求状态与控制流解耦,必须用std::variant存储纯数据快照并以纯函数transit驱动迁移;Policy须无状态、静态,禁止捕获this或共享变量;状态不属于对象,而是函数输入。

可重入的 FSM 在 C++ 中不能靠裸类成员函数 + switch 硬编码状态跳转来实现;真正可重入,意味着同一对象能被多个执行流(如不同线程、中断上下文、递归调用)安全驱动,且状态迁移不依赖外部临时变量或隐式调用栈。核心矛盾在于:状态数据必须与控制流解耦,迁移逻辑必须幂等、无副作用、不持有非局部引用。
为什么 std::function + enum class 状态机仍不可重入?
常见写法把状态存在成员变量 m_state,所有 handle_event() 方法直接读写它——这在递归调用或信号中断中会覆盖中间态;若事件处理函数内部又触发新事件(比如 A→B 后立刻发 EventX 导致 B→C),m_state 就被提前改写,原迁移流程断裂。
- 问题本质是「状态存储」和「迁移执行」耦合在同一个对象生命周期内
-
std::function闭包若捕获this或局部引用,会引入隐式共享,破坏重入性 - 即使加
std::mutex,也只解决并发安全,不解决重入性(重入 ≠ 多线程,而是控制流可嵌套)
用 std::variant 存储纯数据状态快照
把每个状态抽象为一个 struct,不含任何函数指针或 this 指针,仅保存当前上下文所需的数据字段;整个 FSM 的“当前状态”用 std::variant<statea stateb statec></statea> 表示。每次事件输入,都基于旧 std::variant 值构造新值,不修改原值。
- 迁移函数签名应为:
std::variant<statea stateb statec> transit(const std::variant<statea stateb statec>& old, const Event& e)</statea></statea> - 所有状态 struct 必须是
trivially_copyable,避免构造/析构副作用 - 不使用
std::visit的访客模式做分发,而用std::holds_alternative<statea>(v)</statea>+ if-else 显式分支,避免异常路径干扰重入语义
如何让状态行为与对象实例解耦?用策略模板参数
FSM 行为(比如进入 StateB 时要打印日志、启动定时器)不应写死在状态 struct 里,而应由使用者通过模板参数注入。定义主模板:template<typename policy> class ReentrantFsm</typename>,Policy 提供静态成员函数 on_enter_StateB(const StateB&)、on_event_X_in_B(const StateB&, const EventX&) 等。
立即学习“C++免费学习笔记(深入)”;
- Policy 必须是无状态的
struct,所有函数为static,不持有任何成员变量 - FSM 类本身不继承 Policy,也不保存 Policy 实例,只在需要时以
Policy::on_enter_XXX(...)方式调用 - 这样同一个
ReentrantFsm<logpolicy></logpolicy>和ReentrantFsm<testpolicy></testpolicy>可共用同一套状态定义和迁移逻辑,彻底解耦
实际调用时必须传入完整状态快照,禁止隐式 this
对外暴露的唯一驱动接口是纯函数式风格:auto next = fsm.step(current_state, event);。用户负责保存和传递 current_state —— 它可能是栈变量、协程局部变量,甚至跨线程消息体的一部分。
- 绝不提供
void handle(Event)这类修改内部状态的方法 - 若需响应结果(比如迁移后要释放资源),Policy 的
on_exit_XXX函数应返回一个std::optional<cleanupaction></cleanupaction>,由调用方决定是否执行,而非在 FSM 内部触发 - 错误处理统一用
std::expected<nextstate error></nextstate>(C++23)或自定义 union,避免异常抛出打断重入链
真正的难点不在状态建模,而在于说服自己放弃“对象拥有状态”的直觉——可重入 FSM 的状态不属于任何对象,它只是函数输入;所谓“FSM 对象”,其实只是一个迁移规则集合的命名空间载体。最容易被忽略的是:Policy 中任何看似 harmless 的 static std::atomic_int counter 都会破坏重入性,因为重入调用共享该计数器,导致行为不可预测。


















