在 C++ 20 之前,我们往往使用 boost::variant 或手写 union + tag 的方式来实现“状态机”或“多态容器”。随着标准库引入 std::variant,我们可以在保持类型安全、零成本的前提下,构建更易维护、可读性更好的状态机。本文将从概念入手,给出一个完整的示例,并讨论常见的性能与可维护性问题。
1. 什么是状态机?
状态机(State Machine)是一种描述系统在任意时刻只处于有限个状态之一,并根据事件触发状态转换的抽象模型。典型应用包括:网络连接状态、GUI 控件生命周期、协议解析器等。
2. 为何使用 std::variant?
- 类型安全:编译期保证只能存放预定义的几种类型。
- 无运行时开销:
std::variant内部实现类似于联合体,只有一个成员活跃。 - 访问方式多样:
std::visit、std::get_if等提供了灵活的访问手段。 - 异常安全:在异常发生时,variant 保证内部状态一致。
3. 设计思路
- 定义状态
将每一种状态映射为一个结构体或类,包含该状态需要的数据。 - 构建 Variant
用using State = std::variant<IdleState, ConnectingState, ConnectedState, ErrorState>; - 状态转换
通过事件触发函数,对当前状态做std::visit,返回新的状态。 - 事件循环
可以用一个简单的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