精读 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(媒体播放机) 的状态机,用三种状态:StoppedPlayingPaused。每个状态类实现对 play() / pause() / stop() 的响应,并通过 Context(这里是 MediaPlayer)执行状态切换。

cpp 复制代码
// 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 -> Playing
    player.pause();  // Playing -> Paused
    player.play();   // Paused -> Playing
    player.stop();   // Playing -> Stopped
    player.pause();  // 无效
    return 0;
}

优点(State Pattern)

  • 每个状态的逻辑局部化,便于维护、单元测试与扩展。
  • 增加新状态无需改动大量 switch-case,开放/封闭性好。
  • 可以在状态对象中保存状态相关数据(如果需要)。

缺点

  • 如果状态很多,会产生大量类,增加复杂度(但可以用单例或共享实例减少开销)。
  • 状态对象之间切换需要 Context 提供切换接口,设计上要小心避免循环依赖。

如何设计状态机

设计状态机不是随手写个 enum 就完事,下面是实战建议:

  1. 明确领域上的"状态"与"事件"
    • 列出对象可能的状态(名词)。
    • 列出能触发变化的事件(动词/消息)。
  2. 画出状态图(最重要)
    • 把状态画成节点,事件作为边(标注 guard/动作/entry/exit)。
    • 标注 entry/exit 以及延时触发 (timer) 的边。
  3. 区分转换类型
    • 外部转换(leave + enter):先执行 exit,再 transition,再 entry。
    • 内部转换(stay + action):在同一状态内响应事件,不触发 exit/entry。
  4. 定义 Guard 与 Actions
    • Guard:条件判断(例如用户权限、资源可用性)。
    • Action:转换时需要执行的副作用(日志、网络调用、排队等)。
  5. 考虑并行(Orthogonal)区域
    • 对于复杂对象,可能需要多个互不干扰的子状态机(例如播放器既有播放状态,也有网络状态)。把它们做成并行 region,会比把所有组合列举更清晰。
  6. Entry / Exit handlers
    • 把资源分配/释放放到 entry/exit,可以避免状态切换时资源泄漏。
  7. 测试策略
    • 对每一条边写单元测试(从状态A触发事件E应该进入状态B并产生动作X)。
    • 使用表驱动测试(state,event -> expected_state, expected_action)。
  8. 性能与持久化
    • 若状态机在高频路径,prefer switch/enum 或零分配实现;若可维护性优先用 State Pattern。
    • 如需持久化(checkpoint),只序列化当前 state id + 必要上下文。

基于开关的状态机(switch / enum) + 扩展

基于 enum + switch 的实现是最直观、也最常见的做法:把状态放在一个枚举里,接收事件时使用 switch(current_state) 跳转。适用于状态相对较少、转换逻辑简单、性能敏感的场景。

下面是 交通信号灯(Traffic Light) 的简单示例,包含定时转换与紧急事件。

cpp 复制代码
// 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 -> Green
    tl.on_event(Event::Timer); // Green -> Yellow
    tl.on_event(Event::Emergency); // Yellow 无变更
    tl.on_event(Event::Timer); // Yellow -> Red
    return 0;
}

优点(switch-based)

  • 直观、简单、性能高(没有虚调用开销)。
  • 易于集中查看所有转换(在一个 switch 里)。

缺点

  • 当状态或事件变多时 switch 会变得臃肿(逻辑散落、难以维护)。
  • 不利于按状态封装复杂行为,扩展性差(每新增状态/事件都要改 switch)。

扩展:把基于开关的实现变得更工程化

1) 表驱动(Transition Table)

把转换写成数据(表格)而非代码,支持热插拔策略、容易测试。示例结构:

cpp 复制代码
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、嵌入式设备)。
    • 缺点:实现与调试复杂;可能需借助成熟库。
相关推荐
liuyao_xianhui2 小时前
四数之和_优选算法(C++)双指针法总结
java·开发语言·c++·算法·leetcode·职场和发展
雨落在了我的手上2 小时前
C语言入门(七):写一个关机程序、随机函数的基本认识
c语言·学习
CAir23 小时前
CGO 原理
c++·go·cgo
GilgameshJSS3 小时前
STM32H743-ARM例程13-SDIO
c语言·arm开发·stm32·嵌入式硬件·学习
GilgameshJSS3 小时前
STM32H743-ARM例程8-EXTI外部中断
c语言·arm开发·stm32·单片机·嵌入式硬件·学习
月盈缺3 小时前
学习嵌入式的第四十三天——ARM——I2C
arm开发·学习
磨十三3 小时前
C++ 中的类型双关、union 与类型双关:让一块内存有多个“名字”
开发语言·c++
hsjkdhs3 小时前
C++之类的组合
开发语言·c++·算法
奔跑吧邓邓子3 小时前
【C++实战(57)】C++20新特性实战:解锁C++编程新姿势
c++·实战·c++20·c++20新特性