在现代 C++(C++17 及以后)中,std::variant 提供了一种优雅且类型安全的方式来处理可以是多种不同类型之一的值。它在实现状态机、事件系统以及多态消息传递时尤为有用。下面通过一个具体示例,演示如何利用 std::variant 搭建一个简单的状态机,并说明其优势与常见注意事项。
1. 需求描述
假设我们需要设计一个订单处理系统,订单可以处于以下几种状态:
| 状态 | 说明 |
|---|---|
Created |
订单已创建,但未支付 |
Paid |
订单已支付 |
Shipped |
订单已发货 |
Delivered |
订单已送达 |
Cancelled |
订单已取消 |
每个状态都有自己的数据结构,例如 Created 只需要订单号,Shipped 需要运单号和预计到达时间等。我们希望通过类型安全的方式保证:
- 只能在合法状态之间转换;
- 对不同状态的处理能够利用编译时检查;
- 代码易读、易维护。
2. 设计思路
- 为每个状态定义一个独立的结构体,并为其提供必要的数据成员。
- 使用
std::variant包装所有可能的状态,命名为OrderState。 - 通过
std::visit统一访问状态,并在访问函数中根据实际类型执行对应逻辑。 - 实现状态转换函数,例如
transition_to_payed、ship_order等,内部使用std::holds_alternative判断当前状态是否合法,若合法则替换std::variant的内容。
3. 代码实现
#include <iostream>
#include <variant>
#include <string>
#include <chrono>
#include <iomanip>
// 1. 状态结构体定义
struct Created {
std::string order_id;
};
struct Paid {
std::string order_id;
double amount;
};
struct Shipped {
std::string order_id;
std::string tracking_number;
std::chrono::system_clock::time_point estimated_delivery;
};
struct Delivered {
std::string order_id;
std::chrono::system_clock::time_point delivered_at;
};
struct Cancelled {
std::string order_id;
std::string reason;
};
// 2. 状态枚举(可选)
enum class OrderStatus { Created, Paid, Shipped, Delivered, Cancelled };
// 3. Variant 包装
using OrderState = std::variant<Created, Paid, Shipped, Delivered, Cancelled>;
// 4. 状态机类
class Order {
public:
explicit Order(std::string id) : state(Created{std::move(id)}) {}
// 查看当前状态
void print_status() const {
std::visit([](auto&& s){
using T = std::decay_t<decltype(s)>;
if constexpr (std::is_same_v<T, Created>)
std::cout << "State: Created, Order ID: " << s.order_id << '\n';
else if constexpr (std::is_same_v<T, Paid>)
std::cout << "State: Paid, Order ID: " << s.order_id << ", Amount: $" << s.amount << '\n';
else if constexpr (std::is_same_v<T, Shipped>)
std::cout << "State: Shipped, Tracking #: " << s.tracking_number << '\n';
else if constexpr (std::is_same_v<T, Delivered>)
std::cout << "State: Delivered at " << std::put_time(std::localtime(&std::chrono::system_clock::to_time_t(s.delivered_at)), "%F %T") << '\n';
else if constexpr (std::is_same_v<T, Cancelled>)
std::cout << "State: Cancelled, Reason: " << s.reason << '\n';
}, state);
}
// 1. 付款
bool pay(double amount) {
if (auto* p = std::get_if <Created>(&state)) {
state = Paid{p->order_id, amount};
return true;
}
std::cerr << "支付失败:只能在 Created 状态下支付。\n";
return false;
}
// 2. 发货
bool ship(const std::string& tracking) {
if (auto* p = std::get_if <Paid>(&state)) {
state = Shipped{p->order_id, tracking, std::chrono::system_clock::now() + std::chrono::hours(48)};
return true;
}
std::cerr << "发货失败:只能在 Paid 状态下发货。\n";
return false;
}
// 3. 送达
bool deliver() {
if (auto* p = std::get_if <Shipped>(&state)) {
state = Delivered{p->order_id, std::chrono::system_clock::now()};
return true;
}
std::cerr << "送达失败:只能在 Shipped 状态下送达。\n";
return false;
}
// 4. 取消
bool cancel(const std::string& reason) {
if (std::holds_alternative <Created>(state) || std::holds_alternative<Paid>(state)) {
if (auto* p = std::get_if <Created>(&state))
state = Cancelled{p->order_id, reason};
else if (auto* p = std::get_if <Paid>(&state))
state = Cancelled{p->order_id, reason};
return true;
}
std::cerr << "取消失败:只能在 Created 或 Paid 状态下取消。\n";
return false;
}
private:
OrderState state;
};
int main() {
Order order("ORD12345");
order.print_status(); // Created
order.pay(99.99); // 转到 Paid
order.print_status();
order.ship("TRK987654321"); // 转到 Shipped
order.print_status();
order.deliver(); // 转到 Delivered
order.print_status();
// 尝试非法操作
order.cancel("Customer request"); // 失败
}
关键点说明
-
类型安全
std::variant通过std::get_if与std::holds_alternative在编译期和运行期均可检查当前类型,避免了裸指针或裸整数带来的错误。 -
可扩展性
新增状态只需定义新结构体并在visit与状态转换函数中相应扩展即可,且不会影响已有代码。 -
模式匹配
std::visit+if constexpr结合std::is_same_v实现了类似模式匹配的效果,简化了switch或if-else链的写法。 -
错误处理
状态机函数返回bool以指示操作是否成功,错误信息通过std::cerr打印。实际项目可替换为日志框架或异常。
4. 与传统实现对比
| 方案 | 代码复杂度 | 编译时检查 | 运行时安全 | 可读性 |
|---|---|---|---|---|
基于 enum + switch |
低 | 低 | 低(可能忘记处理某个枚举) | 中 |
继承自基类 + virtual |
中 | 低(需要 RTTI 或 dynamic_cast) |
中 | 中 |
std::variant + visit |
中 | 高 | 高 | 高 |
std::variant 在现代 C++ 项目中往往是最优雅、最安全的选择,尤其适用于状态机、事件系统以及需要强类型保证的业务逻辑。
5. 小结
本文演示了如何使用 std::variant 为订单状态机提供类型安全的实现。通过把每种状态单独封装成结构体,并在 Order 类中管理 std::variant,可以:
- 在编译期捕获错误;
- 以简洁的方式访问状态数据;
- 方便地扩展新状态或修改已有状态的字段。
如果你正在设计需要多状态或多类型数据的系统,强烈建议尝试 std::variant。它能让代码更可靠、更易维护,且符合现代 C++ 的“类型安全第一”理念。