枚举+switch实现最轻量fsm:用enum class定义连续从0开始的状态,switch处理转移;动态配置用std::map存储{状态,事件}→下一状态映射;避免shared_ptr循环引用,观察者用weak_ptr;注意边界条件与线程安全。

用枚举 + switch 实现最轻量的 FSM
直接用 enum class 定义状态,配合 switch 处理状态转移,是 C++ 里最直观、零依赖的写法。适合状态数少(
常见错误是把状态变量声明为全局或裸指针,导致多实例冲突或生命周期失控;正确做法是封装进类里,状态作为私有成员。
-
enum class State { Idle, Running, Paused, Stopped };—— 强类型枚举,避免隐式转换 - 在类中用
State current_state_ = State::Idle;初始化,别用int或未初始化值 -
switch分支必须覆盖所有枚举值,或加default:并触发断言(如assert(false);),否则未处理状态会静默跳过 - 状态变更时,建议先校验合法性(比如不能从
Stopped直接切到Paused),再赋值,避免非法跃迁
用函数指针数组替代 switch 提升可读性
当状态数增多、每个状态的处理逻辑变长,大块 switch 会难维护。用函数指针数组把“状态 → 行为”映射显式化,更易测试和替换。
注意:函数签名必须严格一致,且数组大小要与枚举值范围对齐,否则越界访问风险高。
- 定义统一行为函数类型:
using StateHandler = void (MyFsm::*)(); - 声明数组:
static constexpr StateHandler handlers_[4] = { &MyFsm::handle_idle, &MyFsm::handle_running, &MyFsm::handle_paused, &MyFsm::handle_stopped }; - 调用时用
(this->*handlers_[static_cast<size_t>(current_state_)])();</size_t>—— 注意static_cast避免编译警告 - 枚举值必须从 0 开始连续(如
Idle=0, Running=1),否则数组索引错位
std::map 实现动态可配置的状态转移表
如果状态转移规则需要运行时修改(比如加载 JSON 配置、支持热更新),硬编码的 switch 或函数数组就不够用了。用 std::map 存储 { 当前状态, 事件 } → 下一状态 映射最灵活。
性能代价明显:每次转移都要两次哈希查找(状态 + 事件组合键),且 map 插入/查找非 noexcept,异常安全需额外考虑。
- 键类型推荐
std::pair<state event></state>(Event也是枚举),避免字符串键带来的分配开销 - 插入转移规则:
transitions_[{State::Running, Event::Pause}] = State::Paused; - 查询时用
auto it = transitions_.find({current_state_, event});,检查it != transitions_.end()再取it->second - 别忘了提供默认转移兜底(比如未定义事件时保持原状态),否则逻辑可能卡死
避免 shared_ptr 循环引用导致 FSM 对象无法析构
当 FSM 类持有回调函数(比如状态进入时通知观察者),而观察者又持有 FSM 的 std::shared_ptr,极易形成循环引用——对象永远不被释放。
这不是 FSM 特有陷阱,但实践中高频发生,尤其在 GUI 或异步任务调度场景。
- 观察者回调中,用
std::weak_ptr持有 FSM,调用前先lock()判断是否还存活 - 避免在 FSM 成员函数里直接绑定
this到std::function,改用 lambda 捕获weak_from_this()(需继承std::enable_shared_from_this) - 若不需要共享所有权,优先用原始指针或引用传参,而非
shared_ptr - 用
valgrind --leak-check=full或 ASan 验证对象析构时机,别只靠日志判断
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











