状态机核心采用enum class+std::variant替代继承,实现值语义、零开销、编译期确定;状态转移用free function+std::visit模式匹配;状态持有用std::optional避免未初始化;资源由外部管理,状态只读取不释放。

状态机核心:用 enum class + std::variant 替代继承
直接继承 State 基类再派生多个子类,是传统 FSM 最容易导致耦合的方式——每个新状态都要改基类、加虚函数、牵连头文件依赖。现代 C++ 更轻量的做法是把所有状态定义为 enum class,再用 std::variant 封装状态数据,让状态本身成为值语义对象,而非运行时多态对象。
这样做的关键好处是:状态切换不依赖虚函数调用,无 vtable 开销;状态数据可按需携带(比如 ConnectingState 需要 timeout_ms,而 IdleState 无需任何字段),避免“空字段污染”;所有状态类型在编译期确定,IDE 能跳转、静态分析能覆盖。
常见错误是误把 std::variant 当成“万能容器”往里塞任意类型——它只接受显式列出的类型,且必须保证每个类型可构造、可析构。建议先定义好全部状态类型,再声明 using State = std::variant<idlestate connectingstate connectedstate errorstate>;</idlestate>。
状态转移逻辑:用 free function + pattern matching 处理事件
别把状态转移逻辑塞进每个状态类的 handle() 成员函数里。那样会迫使状态类知道其他状态的存在,破坏封装。更解耦的做法是写一个独立的自由函数,比如 next_state(),接收当前 State 和事件(如 ConnectEvent 或 TimeoutEvent),返回新的 State。
立即学习“C++免费学习笔记(深入)”;
利用 C++17 的 std::visit 实现模式匹配:
State next_state(const State& s, const Event& e) {
return std::visit([](const auto& state, const auto& event) -> State {
using T = std::decay_t<decltype(state)>;
if constexpr (std::is_same_v<T, IdleState>) {
if (std::holds_alternative<ConnectEvent>(event)) return ConnectingState{.retries = 3};
else return state;
}
else if constexpr (std::is_same_v<T, ConnectingState>) {
if (std::holds_alternative<ConnectedEvent>(event)) return ConnectedState{};
else if (std::holds_alternative<TimeoutEvent>(event)) return IdleState{};
else return state;
}
// ... 其他分支
}, s, e);
}
注意点:
-
std::visit要求所有重载分支返回相同类型,所以每个 lambda 分支都得明确返回State - 不要在 lambda 里写复杂逻辑——只做决策,把副作用(如日志、IO)移到外部调用处
- 事件类型也建议用
std::variant统一管理,避免散落的int或string事件码
状态持有与更新:用 std::optional 避免未初始化陷阱
直接声明 State current_state; 会导致默认构造问题——如果某个状态类型没有默认构造函数(比如 ConnectingState 必须带参数),编译就失败。更安全的方式是用 std::optional<state></state>,显式表达“尚未进入任何状态”的合法中间态。
典型用法:
- 初始化时保持
std::nullopt,直到首次调用enter_idle()等入口函数 - 每次状态转移后,用
current_state = next_state(*current_state, event);更新 - 对外暴露
has_state()或get_if<ConnectedState>(&*current_state)来查询当前状态细节
容易踩的坑:忘记检查 current_state.has_value() 就直接解引用,触发未定义行为。建议把状态访问封装成小函数,内部做断言或返回 std::nullopt。
事件分发与生命周期:避免在状态内 new/delete 对象
状态机本身不该负责资源管理。比如“连接中”状态需要启动一个 std::thread 或创建 asio::deadline_timer,这些对象应由外部拥有者(如 ConnectionManager)创建并传入事件,而不是在 ConnectingState 构造函数里 new 出来。
理由很实际:
- new 出来的对象生命周期难追踪,容易内存泄漏或 use-after-free
- 测试时无法 mock 这些依赖,状态类变得不可测
- 状态切换时若没显式销毁,资源残留会干扰下一个状态
正确做法是让事件携带句柄(如 std::shared_ptr<Timer> 或裸指针 + 明确所有权契约),状态只读取、不释放。真正销毁时机由状态机外部统一控制——比如整个 FSM 销毁时,才清理所有关联资源。
复杂点在于:状态数据和外部资源之间的生命周期绑定必须清晰。最容易被忽略的是异步回调捕获了已销毁的状态对象,结果回调触发时访问 dangling reference。解决办法不是加锁或 shared_ptr 套娃,而是确保回调执行前,状态机已明确退出该状态,并切断所有外部引用。



















