应仅在状态携带异构、专属、不可共享数据时用std::variant实现FSM;纯枚举跳转反增开销。核心判据是状态是否需类型互不兼容的字段,如ConnectingState含string/int、ConnectedState含sockfd/timeout、DisconnectedState仅error_code,此时variant可避免optional堆砌与运行时判空,编译期强制合法访问。

直接用 std::variant 实现有限状态机(FSM)是可行的,但只在状态携带**异构、专属、不可共享**的数据时才真正值得——纯枚举跳转(如 Idle/Running/Done 三个空状态)反而引入冗余和编译开销。
什么时候该用 std::variant 而不是 enum class
核心判断标准:状态是否需要附带类型互不兼容的字段。
- ConnectingState 需要
std::string address和int retry_count - ConnectedState 需要
int sockfd和std::chrono::seconds timeout - DisconnectedState 只需
int error_code
此时若硬塞进一个 struct + enum class,就得堆一堆 std::optional 字段,每次访问前必须判空、易漏初始化、字段语义模糊。而 std::variant<connectingstate connectedstate disconnectedstate></connectingstate> 让编译器强制你“只处理当前合法状态及其全部字段”,非法访问在编译期就报错。
std::visit 的 lambda 必须配合 if constexpr 和 std::decay_t
写 std::visit([](auto& s) { ... }, state) 是常见起点,但内部若用普通 if 或 typeid,就等于放弃 std::variant 的核心优势。
立即学习“C++免费学习笔记(深入)”;
- 漏掉
std::decay_t→decltype(s)是引用类型,std::is_same_v<t connectingstate></t>永远为 false - 不用
if constexpr→ 所有分支代码都参与模板实例化,哪怕s是ConnectedState,编译器仍会尝试编译s.address,直接报错 - 正确写法必须是:
using T = std::decay_t<decltype>; if constexpr (std::is_same_v<t connectingstate>) { ... }</t></decltype>
状态转换不能靠裸赋值,得封装 transition 函数
直接写 state = Connecting{host, port} 看似简单,但副作用(如日志、资源申请、前置校验)会散落在各处,难以统一维护。
- 把状态变更逻辑收口到一个
transition函数里,例如transition(state, Connect{host, port}) - 函数内部可基于当前状态做合法性检查(比如禁止从
ErrorState直接跳到ConnectedState) - 副作用(如 socket 创建、重试计数重置)集中在此处,避免重复或遗漏
- 避免依赖
index()或holds_alternative()做主逻辑分支——它们适合调试或日志,不适合驱动状态流转
容易被忽略的细节:默认构造、大对象与空状态语义
std::variant 默认构造会调用第一个类型的默认构造函数,这常被误当作“初始状态”的设计依据,但实际隐患不少。
- 别把
std::monostate塞在using State = std::variant<:monostate ...></:monostate>开头,除非你真需要“无状态”作为合法初值;否则它掩盖了状态建模不完整的问题 - 状态 struct 中若含大对象(如
std::vector<uint8_t></uint8_t>),优先包一层std::unique_ptr<t></t>,避免栈空间暴涨 - 每个状态 struct 应只放其“必要且充分”的字段,不预留、不共享、不妥协;
Idle{}比struct Idle {}更清晰,后续加字段也自然 - 所有状态类型必须满足
std::is_move_constructible_v,否则std::variant赋值可能失败



















