std::variant适用于状态携带异构数据的FSM场景,而非纯枚举跳转;需配合if constexpr与std::decay_t使用std::visit,封装transition函数处理副作用,避免依赖index()或holds_alternative()作主逻辑分支。

直接用 std::variant 实现有限状态机(FSM)是可行的,但不是“套个模板就跑通”的事——它天然适合**状态携带数据且类型各异**的场景,而对纯枚举式跳转(比如只有 Idle/Running/Done 三个空结构体)反而增加冗余。关键在“状态是否需要附带专属字段”。
什么时候该用 std::variant 而不是 enum class
当每个状态本身要携带不同结构的数据时,std::variant 才真正发挥价值。比如:
-
ConnectingState需要存std::string address和重试次数int retry_count -
ConnectedState需要存 socket 句柄int sockfd和心跳超时std::chrono::seconds timeout -
DisconnectedState只需一个错误码int error_code
这时若硬用 enum class + 一堆可选成员变量(std::optional<std::string>, std::optional<int>…),逻辑分散、易漏初始化、访问前必须反复检查 has_value()。而 std::variant<ConnectingState, ConnectedState, DisconnectedState> 让类型系统强制你“每次只处理一种合法状态及其全部字段”,编译期就卡死非法访问。
std::visit 的 lambda 必须用 if constexpr + decay_t
直接写 std::visit([](auto& s) { ... }, state) 是常见错误起点。如果内部用普通 if (typeid(s) == typeid(ConnectingState)) 或 dynamic_cast,不仅低效,还绕过了 std::variant 的核心优势——编译期类型分发。
立即学习“C++免费学习笔记(深入)”;
正确写法必须配合 if constexpr 和 std::decay_t:
std::visit([](auto& s) {
using T = std::decay_t<decltype(s)>;
if constexpr (std::is_same_v<T, ConnectingState>) {
std::cout << "Connecting to " << s.address << "\n";
} else if constexpr (std::is_same_v<T, ConnectedState>) {
std::cout << "Active on fd " << s.sockfd << "\n";
} else if constexpr (std::is_same_v<T, DisconnectedState>) {
std::cout << "Error: " << s.error_code << "\n";
}
}, current_state);
漏掉 std::decay_t 会导致引用类型不匹配;不用 if constexpr 会让所有分支代码都参与编译,哪怕当前 s 是 ConnectedState,编译器仍会尝试实例化对 ConnectingState::address 的访问,直接报错。
状态转换不能靠赋值,得封装 transition 函数
很多人写成 state = ConnectingState{addr, 0}; 就完事,但实际 FSM 中,状态变更往往伴随副作用:清理资源、发日志、触发回调。裸赋值跳过了这些。
推荐把转换逻辑收进一个函数,例如:
struct StateMachine {
std::variant<IdleState, ConnectingState, ConnectedState> state_;
void connect(std::string addr) {
std::visit(overloaded{
[&](IdleState&) {
state_ = ConnectingState{std::move(addr), 0};
log("start connecting...");
},
[&](ConnectingState& s) {
s.address = std::move(addr); // 复用现有状态
s.retry_count = 0;
log("reconnect with new address");
},
[&](ConnectedState& s) {
disconnect_impl(s); // 先断开
state_ = ConnectingState{std::move(addr), 0};
}
}, state_);
}
};
这里用 overloaded 辅助(可手写或用 std::visit 第三方扩展),确保每个分支只处理自己关心的状态,避免漏 case 或误处理。注意:std::variant 赋值会自动析构旧值、构造新值,但你得自己控制业务逻辑顺序。
index() 和 holds_alternative() 是调试利器,但别用于主逻辑
上线代码里尽量别写 if (state_.index() == 1) 或 if (std::holds_alternative<ConnectingState>(state_)) 做分支判断——这等于退化回手动查表,失去 std::visit 的类型安全和可维护性。
它们真正的用途是:
- 单元测试中快速验证当前状态类型:
assert(std::holds_alternative<ConnectedState>(sm.state_)); - 日志打印:
std::cout << "current state index: " << state_.index() << "\n"; - 序列化/调试器插件里反射状态类型
一旦你在业务逻辑里依赖 index(),后续增删状态类型时极易漏改数字常量,引发静默错误。
最易被忽略的一点:状态 struct 的字段必须是 public 且可移动(或可复制),否则 std::variant 构造或赋值时会编译失败;如果某个状态需要独占资源(如 std::unique_ptr),确保其移动语义正确实现——这不是语法糖,是运行时行为的根基。



















