该用 std::variant 而不是 enum class + struct 当且仅当各状态携带互不兼容、无公共子集的专属字段,否则纯枚举更简洁;使用时须配合 if constexpr 和 std::decay_t 进行编译期类型分发,状态变更必须原子赋值,三处定义(类型、跳转、访问)严格同步。

直接用 std::variant 写有限状态机(FSM)是可行的,但只在状态携带**互不兼容的数据结构**时才真正值得——比如 Connecting 要存 int socket_fd 和 std::string host,而 Authenticating 只需要 std::string token。否则,纯枚举跳转(Idle/Running/Done 都是空 struct)反而引入冗余。
什么时候该用 std::variant 而不是 enum class + struct
当每个状态要带专属字段,且这些字段之间没有公共子集时,enum class State { Idle, Connecting, Connected } 就开始失效了。你不得不把所有可能字段塞进一个大 struct,再靠 std::optional<int></int>、std::optional<:string></:string> 拼凑,结果是内存浪费、判空逻辑满天飞、新增状态时极易漏初始化某字段。
- 常见错误现象:
std::get<:string>(state_data)</:string>在当前状态其实是int时抛std::bad_variant_access - 更隐蔽的问题:加了新状态
Reconnecting,但所有if (state == State::Reconnecting)分支里没初始化对应字段,编译器不报错,运行时访问未构造内存 - 一旦状态数据含非平凡类型(如
std::vector、std::unique_ptr),你必须显式写构造/析构/拷贝逻辑,极易遗漏std::move或 double-free
std::visit 必须配合 if constexpr 和 std::decay_t
写 std::visit([](auto& s) { /* ... */ }, state) 是常见起点,但内部若用普通 if (typeid(s) == typeid(Connecting)) 或 dynamic_cast,就绕过了 std::variant 的核心优势——编译期类型分发。
- 正确写法必须用
std::decay_t<decltype></decltype>提取真实类型,否则引用/const 修饰导致std::is_same_v失败 - 必须用
if constexpr,否则即使s是ConnectedState,编译器仍会尝试实例化对ConnectingState::address的访问,直接报错 - 漏掉任一状态分支 → 编译失败,这是好事;但新手常因此删掉
else if改成else,退化回运行时错误
状态变更必须原子赋值,禁止原地修改字段
std::variant 的安全机制依赖“一次切换,完整构造”。你不能对 std::get<connecting>(state)</connecting> 返回的引用做字段级更新,这等于绕过类型系统,让对象处于中间不一致状态。
立即学习“C++免费学习笔记(深入)”;
- 正确做法:状态变更一律用赋值,例如
state = Connecting{new_sock_fd, "127.0.0.1"}或state = std::in_place_type<connected>, remote_addr</connected> - 若需复用旧状态数据(比如从
Connecting进入Connected时保留 socket fd),应在构造新状态时显式传入,而不是靠“共享字段”或指针 - 避免在
variant中放裸指针或std::unique_ptr—— 这会把内存管理责任推给上层,违背std::variant的自动析构优势
最难绷的是三处同步:状态类型的定义、状态跳转逻辑(transition())、状态访问逻辑(std::visit)必须严格一致。新增一个状态,三处都要改;漏一处,不是警告,是 std::bad_variant_access 或未定义行为。这不是设计缺陷,是类型系统在逼你把隐式契约变成显式约束。



















