std::variant比std::unique_ptr更适合封装异构状态,因其零开销、无堆分配、支持std::visit静态分发,且能静态约束合法状态类型;而std::unique_ptr引入虚调用、堆管理、RTTI依赖并破坏可重入性。

为什么 std::variant 比 std::unique_ptr 更适合封装异构状态
因为状态对象生命周期完全由 FSM 控制,不需要多态销毁或运行时类型擦除;std::variant 零开销、无堆分配、支持 std::visit 直接分发,且能静态约束所有合法状态类型。用 std::unique_ptr 反而引入虚函数调用、堆内存管理、RTTI 依赖,还破坏了可重入性(析构可能触发用户代码)。
常见错误:把状态设计成继承体系后强行塞进 std::variant,结果发现 std::variant 无法接受 std::derived_from 的子类 —— 这说明你误用了继承;正确做法是每个状态为独立的 struct,不共享基类。
- 状态类型必须是
trivially_destructible或至少noexcept析构,否则切换状态时异常安全无法保证 - 避免在状态中持有裸指针或非 RAII 资源;若需外部引用,用
std::weak_ptr或 const 引用传入 FSM 构造函数 -
std::variant的index()不可用于逻辑判断 —— 它只是实现细节;永远用std::visit+ lambda 分发
transition() 必须是无状态、无副作用的纯函数
可重入性的核心在于:任意线程可在任意时刻调用 transition(),且多次调用相同输入必须产生相同输出(状态+动作),不能依赖内部计数器、时间戳或全局变量。
典型陷阱:在某个状态的 on_enter() 里修改了 FSM 成员变量,然后下一个 transition() 读它来决定跳转 —— 这让状态转移逻辑隐式耦合到执行顺序,不可重入。
- 所有转移逻辑应集中在一个
constexpr表或switch中,输入是「当前状态 + 事件」,输出是「目标状态 + 可选动作函数对象」 - 动作函数对象(如
std::function)必须捕获仅限于 const 数据,或通过[this]访问 FSM 的只读接口(如get_context()) - 不要在动作里调用
transition()自身 —— 这会形成隐式递归,栈深度失控且难以测试
如何让 FSM 实例真正可重入:避免 this 指针泄漏和内部 mutable
可重入 ≠ 线程安全。一个 FSM 对象被多个线程同时调用 transition() 是危险的,除非它根本不保存任何可变状态 —— 所有“状态”都存在 std::variant 里,所有“上下文”都只读传入。
错误示范:mutable std::mutex mtx; 或 mutable int retry_count = 0; —— mutable 不解决重入问题,只掩盖数据竞争。
- FSM 类本身应是
const正常可调用的:声明为auto transition(event) const,返回新状态+动作,由调用方决定是否更新内部state_ - 若必须就地更新(如嵌入式场景),则用
std::atomic存std::variant的index(),配合compare_exchange_strong原子切换,但此时动作函数必须是无锁的 - 禁止在状态 struct 内定义
operator()并让它修改 FSM 成员 —— 这等于把控制权交给了状态,破坏了 FSM 的统一调度权
一个最小可行的 std::variant-based FSM 片段
下面不是模板库,而是可直接编译的骨架,聚焦状态切换本质:
struct Idle { void on_enter() const { /* ... */ } };
struct Running { int speed; explicit Running(int s) : speed(s) {} };
struct Error { std::string msg; };
<p>using State = std::variant<idle running error>;</idle></p><p>struct Fsm {
State state_ = Idle{};</p><pre class="brush:php;toolbar:false;">template<typename e>
auto transition(E&& event) const -> std::pair<state std::function>> {
return std::visit([&](const auto& s) -> std::pair<state std::function>> {
using T = std::decay_t<decltype>;
if constexpr (std::is_same_v<t idle>) {
if (event == "start") return {{Running{100}}, []{ /* log start */ }};
else return {{Error{"bad event"}}, {}};
} else if constexpr (std::is_same_v<t running>) {
if (event == "stop") return {{Idle{}}, []{ /* cleanup */ }};
else return {{s}, {}};
} else {
return {{s}, {}};
}
}, state_);
}</t></t></decltype></state></state></typename>
};
注意:这个 transition() 返回新状态+动作,不修改自身;调用者负责原子替换 state_ 并执行动作。这才是可重入的起点 —— 所有复杂性被推给使用者,FSM 保持纯粹。
最容易被忽略的一点:状态类型的构造函数必须是 noexcept,否则 std::variant 在赋值/移动时可能抛异常,导致状态切换中途失败且无法回滚。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











