std::variant与状态机编程的核心概念
状态机是一种用于描述系统在不同状态下行为逻辑的设计模式,每个状态对应特定的处理逻辑和关联数据。传统状态机实现常使用枚举加联合体的方式存储状态数据,但联合体存在类型不安全的问题,容易引发未定义行为。C++17引入的std::variant是类型安全的联合体,能够存储预定义类型集合中的任意一种类型,完美契合状态机需要存储不同状态异构数据的需求。

std::variant的基本特性
std::variant是模板类,声明时需要指定可存储的类型列表,同一时间只能存储其中一种类型的实例。访问variant存储的值时,需要通过std::get、std::visit等安全方式,避免错误的类型访问。例如声明一个可存储int、string、double三种类型的variant:
#include <variant>
#include <string>
#include <iostream>
// 定义可存储三种类型的variant
using MyVariant = std::variant<int, std::string, double>;
int main() {
MyVariant v1 = 10; // 存储int类型
MyVariant v2 = "hello"; // 存储字符串类型
MyVariant v3 = 3.14; // 存储double类型
// 安全访问存储的值
std::cout << std::get<int>(v1) << std::endl;
return 0;
}
状态机的核心组成
一个完整的状态机通常包含三个核心部分:状态集合、状态转移规则、每个状态下的处理逻辑。使用std::variant时,我们可以将不同的状态类型定义为variant的可选类型,每个类型可以携带该状态对应的专属数据,状态转移就是替换variant中存储的类型实例,状态处理则通过访问variant当前存储的类型来完成。
基于std::variant的状态机实战实现
我们以一个简易的订单状态机为例,订单存在待支付、已支付、已发货、已取消四个状态,每个状态携带不同的关联数据:待支付状态携带订单金额和创建时间,已支付状态携带支付时间和支付金额,已发货状态携带物流单号和发货时间,已取消状态携带取消原因和取消时间。
第一步:定义状态对应的类型
首先为每个状态定义对应的结构体,结构体包含该状态需要的专属数据:
#include <variant>
#include <string>
#include <chrono>
#include <iostream>
#include <vector>
// 待支付状态数据
struct PendingState {
double order_amount; // 订单金额
std::chrono::system_clock::time_point create_time; // 创建时间
};
// 已支付状态数据
struct PaidState {
std::chrono::system_clock::time_point pay_time; // 支付时间
double pay_amount; // 支付金额
};
// 已发货状态数据
struct ShippedState {
std::string tracking_no; // 物流单号
std::chrono::system_clock::time_point ship_time; // 发货时间
};
// 已取消状态数据
struct CancelledState {
std::string cancel_reason; // 取消原因
std::chrono::system_clock::time_point cancel_time; // 取消时间
};
第二步:定义状态variant类型
将上面定义的所有状态结构体作为std::variant的可选类型,定义订单状态类型:
// 订单状态variant,可存储四种状态对应的结构体 using OrderState = std::variant<PendingState, PaidState, ShippedState, CancelledState>;
第三步:实现状态处理逻辑
使用std::visit配合重载的访问者来处理不同状态下的逻辑,std::visit会根据variant当前存储的类型自动调用对应的处理函数,保证类型安全:
// 状态处理访问者
struct StateHandler {
// 处理待支付状态
void operator()(const PendingState& state) const {
std::cout << "当前状态:待支付" << std::endl;
std::cout << "订单金额:" << state.order_amount << std::endl;
}
// 处理已支付状态
void operator()(const PaidState& state) const {
std::cout << "当前状态:已支付" << std::endl;
std::cout << "支付金额:" << state.pay_amount << std::endl;
}
// 处理已发货状态
void operator()(const ShippedState& state) const {
std::cout << "当前状态:已发货" << std::endl;
std::cout << "物流单号:" << state.tracking_no << std::endl;
}
// 处理已取消状态
void operator()(const CancelledState& state) const {
std::cout << "当前状态:已取消" << std::endl;
std::cout << "取消原因:" << state.cancel_reason << std::endl;
}
};
第四步:实现状态转移逻辑
状态转移本质是替换OrderState中存储的类型实例,我们可以定义一个订单类来管理状态转移:
class Order {
private:
OrderState current_state;
public:
// 初始化为待支付状态
Order(double amount) {
current_state = PendingState{amount, std::chrono::system_clock::now()};
}
// 处理支付操作,转移到已支付状态
void pay(double pay_amount) {
if (std::holds_alternative<PendingState>(current_state)) {
current_state = PaidState{std::chrono::system_clock::now(), pay_amount};
std::cout << "支付成功,状态已转移" << std::endl;
} else {
std::cout << "当前状态不允许支付" << std::endl;
}
}
// 处理发货操作,转移到已发货状态
void ship(const std::string& tracking_no) {
if (std::holds_alternative<PaidState>(current_state)) {
current_state = ShippedState{tracking_no, std::chrono::system_clock::now()};
std::cout << "发货成功,状态已转移" << std::endl;
} else {
std::cout << "当前状态不允许发货" << std::endl;
}
}
// 处理取消操作,转移到已取消状态
void cancel(const std::string& reason) {
if (!std::holds_alternative<ShippedState>(current_state)) {
current_state = CancelledState{reason, std::chrono::system_clock::now()};
std::cout << "取消成功,状态已转移" << std::endl;
} else {
std::cout << "已发货订单不允许取消" << std::endl;
}
}
// 处理当前状态
void handle_current_state() const {
std::visit(StateHandler{}, current_state);
}
};
第五步:测试状态机运行效果
编写测试代码验证状态机的状态转移和处理逻辑是否正确:
int main() {
// 创建订单,初始为待支付状态
Order order(199.9);
order.handle_current_state();
std::cout << "-------------------" << std::endl;
// 支付订单,转移到已支付状态
order.pay(199.9);
order.handle_current_state();
std::cout << "-------------------" << std::endl;
// 发货,转移到已发货状态
order.ship("SF123456789");
order.handle_current_state();
std::cout << "-------------------" << std::endl;
// 尝试取消已发货订单,应该失败
order.cancel("不想要了");
order.handle_current_state();
return 0;
}
std::variant状态机的优势与注意事项
核心优势
- 类型安全:相比传统联合体,variant会在编译期检查类型访问的合法性,避免错误的类型转换。
- 代码简洁:不需要使用大量的枚举和条件判断来关联状态和数据,状态和数据的绑定更加自然。
- 扩展方便:新增状态时只需要新增对应的结构体类型,添加到variant的类型列表中,再补充对应的处理逻辑即可,符合开闭原则。
注意事项
- variant的类型列表是固定的,无法在运行时动态添加可存储的类型,因此适合状态类型固定的场景。
- 访问variant时必须处理所有可能的类型,否则编译会报错,使用std::visit时如果访问者没有覆盖所有类型会触发编译错误。
- 如果variant存储的类型包含自定义类型,需要确保自定义类型有正确的拷贝、移动构造函数,避免资源管理问题。
适用场景总结
std::variant实现的状态机非常适合状态数量固定、不同状态需要携带不同异构数据的场景,比如协议解析、游戏角色状态管理、业务流程状态流转等。如果状态数量非常多或者状态类型需要动态变化,可能需要结合其他方式扩展,但大部分中小型状态机场景都可以通过std::variant实现得简洁且安全。
C++_std::variant状态机编程异构数据现代C++实战修改时间:2026-06-24 08:30:45