应仅在状态数据互不兼容且共享字段少于50%时用std::variant实现fsm;否则宜用enum class。需用if constexpr+std::decay_t配合std::visit全覆盖分支,状态变更必须原子赋值新对象而非修改字段。

直接用 std::variant 实现有限状态机(FSM)只有在状态携带**互不兼容、结构差异显著的数据**时才真正值得;否则,它只会增加编译负担、模糊语义、掩盖设计问题。
什么时候该用 std::variant 而不是 enum class
核心判断就一条:字段是否“按状态分裂”。如果每个状态需要的字段类型和数量完全不同,且共享字段少于 50%,std::variant 就是更干净的选择。
- ConnectingState 需要
std::string address和int retry_count - ConnectedState 需要
int sockfd和std::chrono::seconds timeout - IdleState 真的什么也不需要 → 定义为
struct Idle {};,不是std::monostate - 硬塞进一个
enum class State+ 大struct堆满std::optional字段 → 容易漏初始化、反复判空、内存浪费 - 用裸
union或手动reinterpret_cast→ 析构时机不可控,C++17 后已无必要
std::visit 必须覆盖所有分支,且必须用 if constexpr + std::decay_t
写错一个分支,编译直接失败——这不是 bug,是保护机制。但新手常因此退化成运行时错误。
- 漏掉任一状态类型(比如新增了
Reconnecting却没在std::visit里加分支)→ 编译报错:no matching function for call to visit - 参数写成
auto&& s而非const auto& s→ 可能意外触发移动语义,尤其当状态含std::string或std::vector - 不用
std::decay_t<decltype></decltype>→ 引用类型不匹配,std::is_same_v<t connecting></t>永远为 false - 不用
if constexpr→ 所有分支代码都参与编译,哪怕当前是Connected,编译器仍会尝试访问Connecting::address,直接报错 - 示例正确写法:
std::visit([](const auto& s) { using T = std::decay_t<decltype>; if constexpr (std::is_same_v<t idle>) { // do nothing } else if constexpr (std::is_same_v<t connecting>) { std::cout ) { std::cout </t></t></decltype>
状态变更不能靠字段级修改,必须原子赋值新对象
状态切换不是“改几个字段”,而是“整个状态值的替换”。中间不一致状态在异常路径下极易引发未定义行为。
- 禁止对
std::get<connecting>(state)</connecting>返回的引用做字段更新 → 绕过std::variant的类型安全机制 - 禁止先调
state.index() = 1再手动构造对象 →std::variant内部可能持有未初始化或已析构成员 - 正确做法:用赋值构造完整新状态,例如
state = Connecting{fd, "127.0.0.1"}或state = std::in_place_type<connected>, fd, timeout</connected> - 若需复用旧数据(如从
Connecting进入Connected时保留 socket fd),显式传入,不要靠“共享字段”或指针传递 - 避免在
std::visitlambda 内做状态变更 →std::visit是只读访问,状态更新逻辑应收口到独立的transition()函数中
最难绷的一点:很多人以为定义好 std::variant<a b c></a> 就万事大吉,却忘了状态定义、跳转逻辑、访问逻辑三者必须严格同步——新增一个状态,漏改任意一处,轻则编译失败,重则运行时崩溃。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











