状态机实现应依复杂度选方案:简单场景用带防护的switch,中等规模用表驱动,数据异构时用std::variant+访问者;始终重视资源生命周期与状态同步。

用 switch 写状态机:简单但容易失控
直接在函数里用 switch 处理状态跳转,是 C++ 里最常见也最容易写出 bug 的写法。它适合只有 3–4 个状态、逻辑极简的场景,比如一个按钮去抖状态机。
常见错误现象:case 忘写 break 导致意外穿透;状态变量被多处修改,没人知道当前到底在哪个状态;新增状态时漏改 default 分支,运行时崩溃却没提示。
实操建议:
- 每个
case块末尾强制加break,哪怕后面跟着default - 把状态枚举定义成
enum class State { Idle, Running, Paused };,避免隐式整型转换 - 在
default:分支里加assert(false)或抛异常,别让它静默吞掉非法状态 - 别在
switch里做耗时操作(比如文件读写),状态机函数应该快进快出
表驱动状态机:用数组/映射替代硬编码逻辑
当状态超过 5 个、事件类型多于 3 种,或者需要运行时动态加载行为时,switch 就撑不住了。表驱动的核心是把“当前状态 + 输入事件 → 下一状态 + 动作”这个映射关系抽出来,存在二维数组或 std::map 里。
立即学习“C++免费学习笔记(深入)”;
性能与兼容性影响:查表本身开销极小,但若用 std::map<:pair event>, Transition></:pair>,每次查找是 O(log n);而用 std::array<:array n_events>, N_STATES></:array> 是纯 O(1),但要求状态和事件数量固定且不大。
实操建议:
- 优先用
std::array配合enum class底层类型(如enum class State : uint8_t { ... };),保证索引安全 - 动作函数指针统一为
void (*)()或std::function<void></void>,避免在表里塞大量if分支 - 初始化表时用
= {}全零初始化,未覆盖项默认为“非法转移”,运行时可快速捕获配置错误
std::variant + 访问者模式:现代 C++ 的类型安全方案
如果你的状态携带不同数据(比如 Connecting 要存 socket 句柄,Authenticating 要存 token 字符串),用单一整型状态变量就会不断 cast、判空、容错,非常脆弱。这时 std::variant 能让每个状态自带专属数据,编译期强制处理所有分支。
容易踩的坑:std::visit 的 lambda 必须覆盖 std::variant 所有类型,少一个编译不过;状态变更不能只改数据,还得确保访问逻辑同步更新,否则运行时 std::bad_variant_access。
实操建议:
- 把状态定义为
using State = std::variant<idle connecting connected error>;</idle>,每个 struct 只放该状态真正需要的字段 - 用
std::visit([](const auto& s) { /* 处理每种状态 */ }, state)替代switch,编译器会帮你检查遗漏 - 状态迁移用成员函数
State::next(Event e)返回新State,不暴露内部std::variant操作细节
别忽略状态机的生命周期管理
状态机不是写完就能跑的独立模块。它往往嵌在类里,和资源绑定(比如 socket、线程、定时器)。最容易被忽略的是:状态变更时,旧状态持有的资源是否释放?新状态需要的资源是否就绪?
常见错误现象:从 Connected 切到 Disconnecting 后,socket 已关,但后续事件还在往已关闭句柄发数据;或者 Connecting 状态下超时,回调触发时对象已被析构。
实操建议:
- 在状态类析构函数里显式调用
teardown(),清理定时器、取消 pending I/O - 用
std::weak_ptr持有外部依赖(比如 owner 类),避免回调时访问悬空指针 - 所有异步事件入口加
if (state.index() == variant_npos) return;防止状态机已停用还收包
状态机最难的从来不是跳转逻辑,而是状态和资源的耦合边界划在哪——多看两遍析构顺序和回调栈,比多写十个 case 更重要。











