TinyFSM 使用指南:轻量级C++有限状态机
TinyFSM 是一个头文件式的C++11有限状态机库,以极简设计、零依赖、高性能著称,特别适合资源受限或实时性要求高的场景。它没有复杂的配置文件,所有逻辑都在代码中完成。
📦 一、安装与集成
TinyFSM 是纯头文件库,只需两步即可使用:
-
获取源码
git clone https://github.com/digint/tinyfsm.git -
包含头文件
将tinyfsm/include目录加入项目包含路径,在代码中引入:#include "tinyfsm.hpp"编译时添加
-std=c++11及-I参数即可。
🧱 二、核心概念与架构
TinyFSM 的代码结构遵循事件→状态机→状态→转换的清晰脉络:
事件 (Event) ——派生自→ tinyfsm::Event
状态机基类 ——派生自→ tinyfsm::Fsm<自身>
具体状态类 ——派生自→ 状态机基类
状态转换 ——通过→ transit<目标状态>()
初始状态定义 ——通过→ FSM_INITIAL_STATE(基类, 初始状态)
事件分发 ——通过→ dispatch(事件) / FsmList::dispatch()
关键设计理念:
- 事件驱动:状态迁移由事件触发,事件可携带参数
- 当前状态唯一:事件只会被当前活动状态处理,而非广播给所有状态
- 编译时多态:优先使用非虚函数提升性能,虚函数仅在有继承重写需求时使用
- 避免循环依赖:状态机间事件流向应保持单向
🚀 三、五步构建状态机
第1步:定义事件
事件类继承自 tinyfsm::Event,可携带任意成员数据:
struct SwitchEvent : tinyfsm::Event { }; // 无参事件
struct EvMove : tinyfsm::Event {
int target_floor; // 带数据的事件
EvMove(int floor) : target_floor(floor) { }
};
第2步:定义状态机基类
继承 tinyfsm::Fsm<自身>,声明所有状态共有的事件响应接口:
class SwitchFSM : public tinyfsm::Fsm<SwitchFSM> {
public:
/* 默认处理:未定义的事件直接忽略 */
void react(tinyfsm::Event const &) { }
/* 需各状态实现的事件响应(虚函数允许重写) */
virtual void react(SwitchEvent const &) = 0;
virtual void react(EvMove const &) = 0;
/* 进入/退出动作(默认为空) */
virtual void entry() { }
virtual void exit() { }
/* 可选:获取当前状态标识(便于调试) */
virtual std::string getStateName() const = 0;
};
性能提示:所有状态响应一致的事件(如报警),声明为非虚函数可避免vtable查找。
第3步:定义具体状态类
每个状态继承自状态机基类,实现特定的事件响应和进入/退出动作:
/* 状态:关闭 */
class OffState : public SwitchFSM {
public:
void entry() override {
std::cout << ">> 进入状态: 关闭" << std::endl;
}
void exit() override {
std::cout << "<< 离开状态: 关闭" << std::endl;
}
void react(SwitchEvent const &) override {
std::cout << " 收到切换事件 -> 开" << std::endl;
transit<OnState>(); // ⚠️ 必须是最后一行
}
void react(EvMove const &) override { /* 忽略 */ }
std::string getStateName() const override { return "Off"; }
};
/* 状态:开启 */
class OnState : public SwitchFSM {
public:
void entry() override {
std::cout << ">> 进入状态: 开启" << std::endl;
}
void react(SwitchEvent const &) override {
std::cout << " 收到切换事件 -> 关" << std::endl;
transit<OffState>();
}
void react(EvMove const & e) override {
std::cout << " 移动至楼层: " << e.target_floor << std::endl;
}
std::string getStateName() const override { return "On"; }
};
两条铁律:
transit<>()必须是反应函数中的最后一条语句(后续代码不会执行)- 禁止在
entry()/exit()内调用transit<>()
第4步:设置初始状态
使用宏 FSM_INITIAL_STATE(状态机基类, 初始状态类):
FSM_INITIAL_STATE(SwitchFSM, OffState)
这行代码必须在全局作用域(任何函数之外)。
第5步:启动与事件分发
int main() {
SwitchFSM::start(); // 启动状态机(调用初始状态的entry)
SwitchFSM::dispatch(SwitchEvent()); // 分发事件:关→开
SwitchFSM::dispatch(SwitchEvent()); // 开→关
SwitchFSM::dispatch(EvMove(5)); // 当前状态(关)忽略移动事件
return 0;
}
🔄 四、高级特性
1. 带动作/条件的转换
transit<>() 支持转换动作(lambda)和条件判断(返回bool的callable):
void react(CallEvent const & e) override {
if (e.floor == current_floor) return;
// 转换动作:决定电梯上行还是下行
auto action = [this, &e] {
if (e.floor > current_floor)
send_event(MotorUp());
else
send_event(MotorDown());
};
// 条件函数(可选):只有门关闭时才能移动
auto condition = [] { return is_door_closed(); };
transit<MovingState>(action, condition);
}
transit<>() 的执行顺序:
- 调用当前状态的
exit() - 执行转换动作(若提供)
- 切换到新状态
- 调用新状态的
entry()
2. 多状态机协同(FsmList)
当系统包含多个状态机时(如电梯的轿厢和马达),使用 FsmList 统一初始化、复位、分发事件:
/* 声明两个状态机 */
class MotorFSM : public tinyfsm::Fsm<MotorFSM> { ... };
class ElevatorFSM : public tinyfsm::Fsm<ElevatorFSM> { ... };
/* 定义状态机列表 */
using FsmHandle = tinyfsm::FsmList<MotorFSM, ElevatorFSM>;
/* 全局事件分发函数 */
template<typename E>
void send_event(E const & ev) {
FsmHandle::template dispatch<E>(ev);
}
int main() {
FsmHandle::start(); // 启动所有状态机
FsmHandle::reset(); // 重置所有状态机
send_event(Alarm()); // 同时通知马达和电梯
}
⚠️ 设计约束:事件应保持单向流动(如电梯→马达),避免循环依赖。
3. 自定义复位初始化
若状态机持有内部变量,需在复位时重置,可重写 reset() 静态方法:
class SwitchFSM : public tinyfsm::Fsm<SwitchFSM> {
public:
static void reset() {
// 复位所有状态实例(若使用了状态内静态变量)
tinyfsm::StateList<OffState, OnState>::reset();
// 重置自定义变量
toggle_count = 0;
}
static unsigned int toggle_count; // 需在cpp文件中定义
};
4. Mealy 与 Moore 风格
TinyFSM 同时支持两种经典状态机模型:
- Moore型:输出仅依赖于当前状态(在
entry()中执行动作) - Mealy型:输出依赖于状态+事件(在
react()中直接执行动作)
上述开关示例分别演示了两种风格。
❗ 五、常见陷阱与最佳实践
陷阱1:误以为事件会广播给所有状态
事实:dispatch() 仅将事件发送给当前活动状态。要同时触发多个状态机,需使用 FsmList。
陷阱2:在转换后编写代码
void react(Event const &) {
transit<NextState>();
cleanup(); // ❌ 永远不会执行!
}
解决:将后续逻辑放入 entry() 或转换动作中。
陷阱3:在 entry/exit 中调用 transit
后果:可能导致状态栈混乱、无限递归。若需进入时立即转换,应重新审视状态设计。
最佳实践速查表
| 需求 | 做法 |
|---|---|
| 事件带参数 | 在事件类中添加成员变量 |
| 状态间共享数据 | 将数据定义为状态机基类的静态成员 |
| 调试输出状态 | 在基类定义 virtual getStateName() |
| 忽略大量事件 | 基类定义 void react(tinyfsm::Event const &) { } |
| 同一状态机复用 | 将状态类定义为模板 |
| 条件转换 | 使用带condition参数的 transit<>() |
| 性能极致优化 | 所有状态无重写的函数声明为非虚 |
TinyFSM 的精髓在于用最直接的C++语法表达状态图。掌握上述五步流程与三点核心约束,即可轻松驾驭从简单开关到复杂多机协同的各种状态机场景。官方示例中的 elevator 项目是理解多状态机协作的最佳起点。
更多推荐
所有评论(0)