C++ 中如何使用 std::variant 实现类型安全的状态机?

在 C++ 20 之前,我们往往使用 boost::variant 或手写 union + tag 的方式来实现“状态机”或“多态容器”。随着标准库引入 std::variant,我们可以在保持类型安全、零成本的前提下,构建更易维护、可读性更好的状态机。本文将从概念入手,给出一个完整的示例,并讨论常见的性能与可维护性问题。

1. 什么是状态机?

状态机(State Machine)是一种描述系统在任意时刻只处于有限个状态之一,并根据事件触发状态转换的抽象模型。典型应用包括:网络连接状态、GUI 控件生命周期、协议解析器等。

2. 为何使用 std::variant?

  • 类型安全:编译期保证只能存放预定义的几种类型。
  • 无运行时开销std::variant 内部实现类似于联合体,只有一个成员活跃。
  • 访问方式多样std::visitstd::get_if 等提供了灵活的访问手段。
  • 异常安全:在异常发生时,variant 保证内部状态一致。

3. 设计思路

  1. 定义状态
    将每一种状态映射为一个结构体或类,包含该状态需要的数据。
  2. 构建 Variant
    using State = std::variant<IdleState, ConnectingState, ConnectedState, ErrorState>;
  3. 状态转换
    通过事件触发函数,对当前状态做 std::visit,返回新的状态。
  4. 事件循环
    可以用一个简单的 while 循环或更复杂的事件总线。

4. 示例代码

#include <variant>
#include <iostream>
#include <string>
#include <chrono>
#include <thread>
#include <optional>

// ---------- 状态定义 ----------
struct IdleState {
    void enter() const { std::cout << "[Idle] 进入空闲状态。\n"; }
};

struct ConnectingState {
    int attempt = 0;
    void enter() const { std::cout << "[Connecting] 尝试连接,尝试次数:" << attempt << "。\n"; }
};

struct ConnectedState {
    std::string server_ip;
    void enter() const { std::cout << "[Connected] 已连接到 " << server_ip << "。\n"; }
};

struct ErrorState {
    std::string reason;
    void enter() const { std::cout << "[Error] 错误原因: " << reason << "\n"; }
};

// ---------- 事件 ----------
enum class Event {
    Start,
    Connected,
    Failed,
    Disconnect,
    Stop
};

// ---------- 状态机 ----------
class ConnectionMachine {
public:
    using State = std::variant<IdleState, ConnectingState, ConnectedState, ErrorState>;
    State state_{IdleState{}};

    void handle(Event e) {
        std::visit([&](auto &s){ handleEvent(s, e); }, state_);
    }

private:
    // 对 IdleState 事件处理
    void handleEvent(IdleState &s, Event e) {
        if (e == Event::Start) {
            state_ = ConnectingState{0};
            std::get <ConnectingState>(state_).enter();
        }
    }

    // 对 ConnectingState 事件处理
    void handleEvent(ConnectingState &s, Event e) {
        if (e == Event::Connected) {
            state_ = ConnectedState{"192.168.1.1"};
            std::get <ConnectedState>(state_).enter();
        } else if (e == Event::Failed) {
            if (++s.attempt < max_attempts_) {
                std::cout << "[Connecting] 重新尝试连接。\n";
                std::this_thread::sleep_for(std::chrono::milliseconds(500));
                handle(Event::Start);   // 递归尝试
            } else {
                state_ = ErrorState{"连接超时"};
                std::get <ErrorState>(state_).enter();
            }
        }
    }

    // 对 ConnectedState 事件处理
    void handleEvent(ConnectedState &s, Event e) {
        if (e == Event::Disconnect || e == Event::Stop) {
            state_ = IdleState{};
            std::get <IdleState>(state_).enter();
        }
    }

    // 对 ErrorState 事件处理
    void handleEvent(ErrorState &s, Event e) {
        if (e == Event::Stop) {
            state_ = IdleState{};
            std::get <IdleState>(state_).enter();
        }
    }

    static constexpr int max_attempts_ = 3;
};

// ---------- 主程序 ----------
int main() {
    ConnectionMachine machine;

    machine.handle(Event::Start);   // 空闲 -> 连接中
    std::this_thread::sleep_for(std::chrono::seconds(1));

    // 模拟连接失败两次,第三次成功
    machine.handle(Event::Failed);
    std::this_thread::sleep_for(std::chrono::seconds(1));
    machine.handle(Event::Failed);
    std::this_thread::sleep_for(std::chrono::seconds(1));
    machine.handle(Event::Connected);  // 成功连接

    std::this_thread::sleep_for(std::chrono::seconds(1));
    machine.handle(Event::Disconnect); // 断开连接

    std::this_thread::sleep_for(std::chrono::seconds(1));
    machine.handle(Event::Stop);       // 进入空闲

    return 0;
}

代码说明

  • 状态结构体
    每个结构体只保留该状态下必需的数据。enter() 用于展示状态进入时的日志,便于调试。

  • 事件枚举
    简化了事件来源,实际项目中可使用更丰富的事件类型。

  • 状态机类

    • state_std::variant,存放当前状态。
    • handle(Event) 通过 std::visit 调用对应状态下的 handleEvent
    • 每个 handleEvent 只负责自己状态的事件转换逻辑,代码模块化,易于扩展。
  • 最大重连次数
    使用静态常量 max_attempts_,避免硬编码。

5. 性能与可维护性

方面 传统实现(union + tag) std::variant
类型安全 需要手动检查 tag 编译期检查
代码量 较多手动判断 通过 visit 简化
运行时开销 variant 常数级
异常安全 需自行保证 内置保证

使用 std::variant 可以显著降低出错概率,特别是在状态数目较多时。它与 std::visit 的组合几乎可以覆盖所有状态机的典型需求。

6. 进阶:自定义访问器

如果状态之间的转换需要访问更多信息,可以在 std::variant 的外层再包装一个 StateMachine 类,提供统一的 current()、`is

()` 等方法。 “`cpp template class StateMachine { public: using Variant = std::variant; template bool is() const { return std::holds_alternative (state_); } template T& current() { return std::get (state_); } // … 其它辅助方法 }; “` ## 7. 结语 通过 `std::variant` 我们可以在保持零运行时开销的同时,获得类型安全、可维护的状态机实现。它与 C++20 的模式匹配功能(`std::match_variant` 等)相辅相成,为现代 C++ 开发提供了强大且简洁的工具。下次你在编写网络协议、GUI 控件或任何需要状态管理的场景时,试试把 `std::variant` 带进来吧!

发表评论