在许多应用场景中,我们需要设计一个状态机来描述对象的不同状态,例如一个订单系统可以处于「待支付」「已支付」「已发货」「已完成」等状态。传统的做法是使用枚举或字符串来表示状态,然后在代码中使用大量的 if/else 或 switch 来处理不同状态下的行为。这样不仅不够类型安全,也不利于维护和扩展。
C++17 引入了 std::variant,它提供了一个类型安全、无运行时开销的联合体。我们可以利用 std::variant 来实现一个可扩展的状态机,每一种状态都对应一个独立的类型,状态转移通过 std::visit 或模式匹配完成。下面给出一个完整的示例,演示如何使用 std::variant 来实现一个订单状态机,并说明其优点。
1. 定义状态类型
首先,为每一种状态创建一个空结构体,作为类型标签。若需要在状态中携带数据,可以在结构体中添加成员。
struct PendingPayment {}; // 待支付
struct Paid {}; // 已支付
struct Shipped { std::string tracking_number; }; // 已发货
struct Completed {}; // 已完成
PendingPayment、Paid、Completed是不携带数据的标记类型。Shipped包含一个tracking_number,演示携带状态数据的用法。
2. 定义订单类
订单类包含一个 std::variant 成员,表示当前状态,并提供状态转移的接口。
#include <variant>
#include <string>
#include <iostream>
#include <stdexcept>
class Order {
public:
using State = std::variant<PendingPayment, Paid, Shipped, Completed>;
Order() : state_(PendingPayment{}) {} // 初始状态为 PendingPayment
// 访问当前状态
const State& state() const { return state_; }
// 触发支付事件
void pay() {
std::visit([this](auto& s) { this->handlePay(s); }, state_);
}
// 触发发货事件
void ship(const std::string& tracking) {
std::visit([this, &tracking](auto& s) { this->handleShip(s, tracking); }, state_);
}
// 触发完成事件
void complete() {
std::visit([this](auto& s) { this->handleComplete(s); }, state_);
}
private:
State state_;
// 事件处理函数,使用重载实现不同状态的逻辑
void handlePay(PendingPayment&) {
state_ = Paid{};
std::cout << "Order paid.\n";
}
void handlePay(Paid&) {
throw std::logic_error("Order is already paid.");
}
void handlePay(Shipped&) {
throw std::logic_error("Cannot pay after shipping.");
}
void handlePay(Completed&) {
throw std::logic_error("Cannot pay after completion.");
}
void handleShip(Paid& s, const std::string& tracking) {
state_ = Shipped{tracking};
std::cout << "Order shipped, tracking: " << tracking << ".\n";
}
void handleShip(PendingPayment&) {
throw std::logic_error("Cannot ship before payment.");
}
void handleShip(Shipped&) {
throw std::logic_error("Order already shipped.");
}
void handleShip(Completed&) {
throw std::logic_error("Cannot ship after completion.");
}
void handleComplete(Shipped& s) {
state_ = Completed{};
std::cout << "Order completed.\n";
}
void handleComplete(PendingPayment&) {
throw std::logic_error("Cannot complete before shipping.");
}
void handleComplete(Paid&) {
throw std::logic_error("Cannot complete before shipping.");
}
void handleComplete(Completed&) {
throw std::logic_error("Order already completed.");
}
};
关键点说明
- 状态存储:
State是std::variant,只包含合法的状态类型。编译器会检查我们是否在任何地方把非法的类型放进去。 - 事件分派:每个事件(
pay、ship、complete)通过std::visit调用对应的处理函数。处理函数通过函数重载实现对不同状态的特化逻辑。 - 错误处理:当事件不合法时抛出异常,保证在错误使用时能得到明确提示。
3. 使用示例
int main() {
Order o;
try {
o.pay(); // OK
o.ship("TRK12345"); // OK
o.complete(); // OK
// o.pay(); // 会抛异常:Cannot pay after completion.
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << '\n';
}
return 0;
}
运行结果:
Order paid.
Order shipped, tracking: TRK12345.
Order completed.
4. 优点与扩展
- 类型安全:
std::variant确保状态只能是预定义的几种类型,任何非法状态都会在编译期被捕获。 - 可维护性:状态转移逻辑集中在对应的处理函数中,易于阅读和修改。添加新状态只需要定义新类型并实现相应的处理函数。
- 无运行时开销:
std::variant是无运行时开销的联合体,类似于枚举,保持高效。 - 状态携带数据:如果状态需要携带数据,只需在对应结构体中添加成员即可。
std::visit可以轻松访问这些成员。
在更复杂的系统中,可以结合模板元编程或状态机库(如 Boost.Statechart)进一步提升表达力和可复用性。但即便是最简单的 std::variant 方案,也已经能够提供比传统枚举更安全、更易维护的状态机实现。