在C++17之后,std::variant 与 std::visit 为实现有限状态机(Finite State Machine, FSM)提供了优雅而类型安全的工具。相比传统的基于枚举和指针的设计,variant 使得状态切换更加显式、错误更难出现。本文从理论到实践,逐步演示如何用 std::variant 构造一个简单但完整的状态机。
1. 状态机的概念与需求
有限状态机由状态、事件和转移规则三部分组成。典型的使用场景包括:
- 网络协议解析(如 HTTP 解析器)
- 交互式游戏角色状态(Idle → Running → Jumping)
- 嵌入式系统的工作模式切换
本例将演示一个简易的“文本编辑器”状态机,它包含三种状态:
Editing:用户正在输入文本ReadOnly:文件以只读方式打开Error:出现错误,无法继续操作
2. 状态类型的定义
#include <variant>
#include <string>
#include <iostream>
struct Editing {
std::string buffer;
};
struct ReadOnly {
std::string buffer;
};
struct Error {
std::string message;
};
这里每个状态都携带自己的数据,便于状态转移时保留上下文。
3. 事件枚举
enum class Event {
OpenReadOnly, // 以只读方式打开文件
StartEdit, // 开始编辑
Save, // 保存文件
Close, // 关闭文件
ErrorOccur // 发生错误
};
事件是无状态的,用枚举来描述所有可能的操作。
4. 转移函数
我们用 std::visit 对当前状态进行匹配,然后根据事件决定下一状态。为了保持可读性,我们为每个状态定义一个专门的 handle 函数。
using State = std::variant<Editing, ReadOnly, Error>;
State handle(const State& current, Event e, const std::string& data = "") {
return std::visit([e, &data](auto&& state) -> State {
using T = std::decay_t<decltype(state)>;
if constexpr (std::is_same_v<T, Editing>) {
switch (e) {
case Event::Save:
// 这里假设保存成功
std::cout << "Saved: " << state.buffer << std::endl;
return state;
case Event::Close:
std::cout << "Closing editor." << std::endl;
return Error{"Editor closed"};
default:
std::cout << "Unhandled event in Editing." << std::endl;
return state;
}
} else if constexpr (std::is_same_v<T, ReadOnly>) {
switch (e) {
case Event::StartEdit:
std::cout << "Cannot edit: read-only mode." << std::endl;
return Error{"Attempted edit in read-only"};
case Event::Close:
std::cout << "Closing read-only editor." << std::endl;
return Error{"Editor closed"};
default:
std::cout << "Unhandled event in ReadOnly." << std::endl;
return state;
}
} else if constexpr (std::is_same_v<T, Error>) {
// 在错误状态下,只能尝试恢复或退出
switch (e) {
case Event::OpenReadOnly:
std::cout << "Recovering to read-only mode." << std::endl;
return ReadOnly{""};
default:
std::cout << "Ignoring event in Error state." << std::endl;
return state;
}
}
}, current);
}
5. 状态机驱动
int main() {
State current = Editing{"Hello World!"};
current = handle(current, Event::Save);
current = handle(current, Event::Close); // 进入 Error 状态
// 在错误状态下尝试恢复
current = handle(current, Event::OpenReadOnly);
// 现在是 ReadOnly 状态,尝试编辑
current = handle(current, Event::StartEdit); // 会触发错误
return 0;
}
6. 关键点回顾
- 类型安全:
std::variant强制在编译期检查所有状态分支,避免忘记处理某个状态。 - 可维护性:每个状态的处理逻辑封装在
if constexpr分支内,结构清晰。 - 扩展性:新增状态只需定义结构体并在
handle中添加对应分支,其他代码不受影响。 - 性能:
std::variant与std::visit内部实现使用跳转表,性能与传统switch类似。
7. 进一步的改进
- 事件携带数据:将事件封装为结构体,携带更多上下文信息。
- 状态机模板化:利用模板把状态类型与转移函数解耦,形成通用的状态机框架。
- 自动生成:结合宏或代码生成工具,从状态/事件表自动生成
handle函数。
结语
通过 std::variant 与 std::visit,C++ 编写的有限状态机既保持了类型安全,又极大提升了代码可读性。掌握这种模式后,你可以在各种需要状态管理的场景中快速构建稳健、易维护的系统。祝你编码愉快!