精读 C++20 设计模式:行为型设计模式 — 状态机模式
精读 C++20 设计模式:行为型设计模式 — 状态机模式
前言
状态机(State Machine)是工程里极常见也极重要的工具:当一个对象的行为不仅仅由当前输入决定,而是和“当前状态”强耦合时,状态机让我们把“状态 + 转换规则 + 动作”结构化、可测试、可扩展。状态机有很多实现风格:面向对象的状态驱动(State Pattern)、基于开关(switch/enum)的实现、表驱动(table-driven)、层次化状态机(statecharts/HSMM)、以及事件驱动的异步状态机等等。
什么是状态机(精简定义)
状态机由三部分组成:
- 状态集(States):对象可能处于的一组离散状态。
- 事件/输入(Events / Inputs):触发状态转换的外部刺激或内部发生的事情。
- 转换(Transitions):在特定条件(Guard)下,从一个状态到另一个状态的迁移,同时可以伴随动作(Action)。
状态机通常还定义:entry/exit 动作(进入/离开某状态时执行)、守卫(guard)(条件判断)、并行(orthogonal)状态区域、以及延时/定时器触发等概念。
状态驱动的状态转换(State Pattern 风格)
这种风格把每个状态封装成一个对象(或类),状态对象负责处理事件并决定是否变更到另一个状态。适合状态行为复杂、每个状态需要大量行为代码时,能把逻辑按状态模块化。
下面演示一个 MediaPlayer(媒体播放机) 的状态机,用三种状态:Stopped
、Playing
、Paused
。每个状态类实现对 play()
/ pause()
/ stop()
的响应,并通过 Context
(这里是 MediaPlayer
)执行状态切换。
// state_pattern_media.cpp — C++20 演示
#include <iostream>
#include <memory>
#include <string>// 前向声明
class MediaPlayer;// 状态接口(只暴露必要事件)
struct State {virtual ~State() = default;virtual void play(MediaPlayer& ctx) = 0;virtual void pause(MediaPlayer& ctx) = 0;virtual void stop(MediaPlayer& ctx) = 0;virtual std::string name() const = 0;
};// Context:持有状态并委托事件
class MediaPlayer {
public:explicit MediaPlayer(std::shared_ptr<State> s) : state_(std::move(s)) {}void set_state(std::shared_ptr<State> s) {std::cout << "[Context] 状态: " << state_->name() << " -> " << s->name() << "\n";state_ = std::move(s);}void play() { state_->play(*this); }void pause() { state_->pause(*this); }void stop() { state_->stop(*this); }
private:std::shared_ptr<State> state_;
};// 具体状态实现
struct StoppedState : State {void play(MediaPlayer& ctx) override;void pause(MediaPlayer& /*ctx*/) override {std::cout << "[Stopped] pause() 无效\n";}void stop(MediaPlayer& /*ctx*/) override {std::cout << "[Stopped] stop() 已是停止状态\n";}std::string name() const override { return "Stopped"; }
};struct PlayingState : State {void play(MediaPlayer& /*ctx*/) override {std::cout << "[Playing] play() 已在播放中\n";}void pause(MediaPlayer& ctx) override;void stop(MediaPlayer& ctx) override;std::string name() const override { return "Playing"; }
};struct PausedState : State {void play(MediaPlayer& ctx) override;void pause(MediaPlayer& /*ctx*/) override {std::cout << "[Paused] pause() 已暂停\n";}void stop(MediaPlayer& ctx) override;std::string name() const override { return "Paused"; }
};// 状态间切换逻辑(实现放后面)
void StoppedState::play(MediaPlayer& ctx) {std::cout << "[Stopped] 开始播放...\n";ctx.set_state(std::make_shared<PlayingState>());
}
void PlayingState::pause(MediaPlayer& ctx) {std::cout << "[Playing] 暂停播放\n";ctx.set_state(std::make_shared<PausedState>());
}
void PlayingState::stop(MediaPlayer& ctx) {std::cout << "[Playing] 停止播放\n";ctx.set_state(std::make_shared<StoppedState>());
}
void PausedState::play(MediaPlayer& ctx) {std::cout << "[Paused] 恢复播放\n";ctx.set_state(std::make_shared<PlayingState>());
}
void PausedState::stop(MediaPlayer& ctx) {std::cout << "[Paused] 停止并回到初始\n";ctx.set_state(std::make_shared<StoppedState>());
}// 使用示例
int main() {auto stopped = std::make_shared<StoppedState>();MediaPlayer player(stopped);player.play(); // Stopped -> Playingplayer.pause(); // Playing -> Pausedplayer.play(); // Paused -> Playingplayer.stop(); // Playing -> Stoppedplayer.pause(); // 无效return 0;
}
优点(State Pattern):
- 每个状态的逻辑局部化,便于维护、单元测试与扩展。
- 增加新状态无需改动大量 switch-case,开放/封闭性好。
- 可以在状态对象中保存状态相关数据(如果需要)。
缺点:
- 如果状态很多,会产生大量类,增加复杂度(但可以用单例或共享实例减少开销)。
- 状态对象之间切换需要 Context 提供切换接口,设计上要小心避免循环依赖。
如何设计状态机
设计状态机不是随手写个 enum
就完事,下面是实战建议:
- 明确领域上的“状态”与“事件”
- 列出对象可能的状态(名词)。
- 列出能触发变化的事件(动词/消息)。
- 画出状态图(最重要)
- 把状态画成节点,事件作为边(标注 guard/动作/entry/exit)。
- 标注 entry/exit 以及延时触发 (timer) 的边。
- 区分转换类型
- 外部转换(leave + enter):先执行 exit,再 transition,再 entry。
- 内部转换(stay + action):在同一状态内响应事件,不触发 exit/entry。
- 定义 Guard 与 Actions
- Guard:条件判断(例如用户权限、资源可用性)。
- Action:转换时需要执行的副作用(日志、网络调用、排队等)。
- 考虑并行(Orthogonal)区域
- 对于复杂对象,可能需要多个互不干扰的子状态机(例如播放器既有播放状态,也有网络状态)。把它们做成并行 region,会比把所有组合列举更清晰。
- Entry / Exit handlers
- 把资源分配/释放放到 entry/exit,可以避免状态切换时资源泄漏。
- 测试策略
- 对每一条边写单元测试(从状态A触发事件E应该进入状态B并产生动作X)。
- 使用表驱动测试(state,event -> expected_state, expected_action)。
- 性能与持久化
- 若状态机在高频路径,prefer switch/enum 或零分配实现;若可维护性优先用 State Pattern。
- 如需持久化(checkpoint),只序列化当前 state id + 必要上下文。
基于开关的状态机(switch / enum) + 扩展
基于 enum
+ switch
的实现是最直观、也最常见的做法:把状态放在一个枚举里,接收事件时使用 switch(current_state)
跳转。适用于状态相对较少、转换逻辑简单、性能敏感的场景。
下面是 交通信号灯(Traffic Light) 的简单示例,包含定时转换与紧急事件。
// switch_state_traffic.cpp
#include <iostream>
#include <chrono>
#include <thread>enum class LightState { Red, Green, Yellow };
enum class Event { Timer, Emergency };struct TrafficLight {LightState state = LightState::Red;int timer_ms = 0;void on_event(Event ev) {switch (state) {case LightState::Red:if (ev == Event::Timer) {std::cout << "Red -> Green\n";state = LightState::Green;} else if (ev == Event::Emergency) {std::cout << "Red + Emergency -> Flashing (保持Red)\n";// 非常简化的策略}break;case LightState::Green:if (ev == Event::Timer) {std::cout << "Green -> Yellow\n";state = LightState::Yellow;} else if (ev == Event::Emergency) {std::cout << "Green + Emergency -> 切换到 Red\n";state = LightState::Red;}break;case LightState::Yellow:if (ev == Event::Timer) {std::cout << "Yellow -> Red\n";state = LightState::Red;}break;}}
};int main() {TrafficLight tl;// 模拟定时器触发tl.on_event(Event::Timer); // Red -> Greentl.on_event(Event::Timer); // Green -> Yellowtl.on_event(Event::Emergency); // Yellow 无变更tl.on_event(Event::Timer); // Yellow -> Redreturn 0;
}
优点(switch-based):
- 直观、简单、性能高(没有虚调用开销)。
- 易于集中查看所有转换(在一个
switch
里)。
缺点:
- 当状态或事件变多时
switch
会变得臃肿(逻辑散落、难以维护)。 - 不利于按状态封装复杂行为,扩展性差(每新增状态/事件都要改 switch)。
扩展:把基于开关的实现变得更工程化
1) 表驱动(Transition Table)
把转换写成数据(表格)而非代码,支持热插拔策略、容易测试。示例结构:
struct Transition {State from;Event on;std::function<bool()> guard; // 可选std::function<void()> action; // 可选State to;
};
运行时逐项匹配 from==current && on==event && (guard()==true)
,执行 action 并切换到 to
。利于把复杂规则序列化到配置文件。
用 std::variant
+ std::visit
表示状态
用 std::variant<StateA, StateB, StateC>
表示状态,配合 std::visit
实现分发,这样能既保留类型安全又避免虚函数开销。
层次化状态机(Hierarchical State Machines / Statecharts)
支持子状态与父状态:若事件在子状态未处理,会向上冒泡到父状态。这能消除重复转换并表达“通用行为在父状态”,常见于 UML 状态图或 SCXML。
并行区域(Orthogonal Regions)
当对象有多个独立属性需要并行状态时,用多个子状态机并行执行,比把所有组合穷举为状态集合更清晰。
超时/定时器与异步事件
状态机常配合定时器(例如:在某状态等待 N 秒后自动切换)或外部异步事件(网络返回、IO 完成)。工程实现通常需要事件队列、工作线程与非阻塞处理。
使用现成库(若项目复杂)
当状态机非常复杂(层次化、多并行区、可视化调试、保存/恢复)时,可以考虑成熟库(如 Boost.SML、Boost.Statechart、SML 或商用引擎) — 在大工程里这些库能显著降低实现与测试成本(选用前评估学习成本与运行时特性)。
总结
我们试图解决什么问题?
- 状态依赖行为多:对象行为不仅依赖于当前输入,还强依赖于“当前状态”。
- 交叉条件复杂:不同状态下相同事件需不同处理;如果把逻辑散落在多个
if/switch
中,难以理解与维护。 - 需要可扩展、可测试、可观察的行为模型:特别是在并发、异步或嵌入式等领域,明确状态与转换能降低 bugs。
我们如何解决问题?
- 面向对象的状态驱动(State Pattern):把状态作为对象,封装行为,实现开闭原则;适合状态行为复杂时。
- 基于开关(enum + switch):直观、高效,适合状态少、性能敏感且逻辑简单的场景。
- 表驱动 / 数据驱动:把转换抽象为数据,便于配置、测试、序列化。
- 层次化/并行化状态机:解决状态组合爆炸问题,提高复用性与表达力。
- 引入事件队列与定时器:处理异步/延时转换,保证系统稳健运行。
方案优点与缺点(对比)
- State Pattern(面向对象)
- 优点:模块化、易维护、易扩展、易单元测试。
- 缺点:类数量增多、若状态非常多实现开销(内存、复杂度)上升。
- Switch / Enum(基于开关)
- 优点:实现简单、性能好、易读(小规模)。
- 缺点:不利于扩展,逻辑会散落、难以模块化;当状态/事件增多会臃肿。
- Table-driven
- 优点:规则集中、易测试、便于序列化与动态配置。
- 缺点:对复杂动作/guard 表达力有限,需要配合函数对象;调试时需良好可观测性。
- Hierarchical / Parallel States
- 优点:表达力强、减少重复、模型更贴合真实系统(例如 GUI、嵌入式设备)。
- 缺点:实现与调试复杂;可能需借助成熟库。