std::variant适用于状态携带差异显著数据的场景:当各状态所需字段结构不同、共享字段少于50%时,用独立struct+variant比enum class+optional更安全高效;否则徒增复杂度。

直接用 std::variant 实现状态机,只在状态携带**不同结构的数据**时才真正值得——比如 ConnectingState 要存地址和重试次数,ConnectedState 要管 socket fd 和超时时间,而 Idle 真的就什么也不需要。纯枚举跳转(只有三个空状态)硬套 std::variant,只会让代码变重、编译变慢、阅读变难。
什么时候该用 std::variant 而不是 enum class
核心判断依据是:字段是否“按状态分裂”。如果每个状态要带的数据结构差异明显,且共享字段少于 50%,std::variant 就是更干净的选择。
- 用
enum class State { Idle, Connecting, Connected };+ 大 struct 塞一堆std::optional字段 → 容易漏初始化、访问前反复判空、内存浪费 - 用裸
union手动管理生命周期 → C++17 后已无必要,且析构极易出错 - 用基类 + 虚函数 → 引入堆分配和虚表开销,对轻量状态机属于过度设计
- 正确姿势:每个状态定义为独立
struct,仅含其必需字段,例如:struct Idle {};<br>struct Connecting { std::string address; int retry_count; };<br>struct Connected { int sockfd; std::chrono::seconds timeout; };<br>using State = std::variant<idle connecting connected>;</idle>
std::visit 必须覆盖所有分支,且要用 if constexpr
写 std::visit 时漏掉一个分支,编译直接失败——这不是缺陷,是保护机制。但新手常因报错去删分支、加 else,结果退化成运行时错误。
- 必须用
std::decay_t<decltype></decltype>提取真实类型,否则引用类型匹配失败 - 必须用
if constexpr,不用的话,哪怕当前是Connected,编译器仍会尝试编译对Connecting::address的访问,直接报错 - 参数推荐用
const auto&,避免意外触发移动语义(尤其当状态里含std::string或std::vector) - 示例写法:
std::visit([](const auto& s) {<br> using T = std::decay_t<decltype>;<br> if constexpr (std::is_same_v<t idle>) {<br> // do nothing<br> } else if constexpr (std::is_same_v<t connecting>) {<br> std::cout } else if constexpr (std::is_same_v<t connected>) {<br> std::cout }<br>}, current_state);</t></t></t></decltype>
状态转换不能靠裸赋值,得封装 transition 函数
直接写 state = Connecting{"127.0.0.1", 3} 看似简单,但副作用(如日志、资源申请、前置校验)会散落在各处,难以维护。
- 禁止在
std::visitlambda 里做状态变更——它只负责“读”,不负责“改” - 把状态迁移逻辑收口到统一函数,例如:
transition_to_connecting(std::string, int),内部完成校验、日志、赋值三件事 - 避免跨状态共享字段(如在
Connecting和Connected里都放std::string host),字段名重复容易引发误读 - 默认构造
State会初始化为第一个类型(如Idle{}),这通常合理;若需“无状态”占位,可用std::monostate,但要慎用——它掩盖了“初始状态未明确定义”的问题
最易被忽略的一点:每个状态 struct 必须满足 std::is_move_constructible_v,否则 std::variant 赋值会静默失败或编译报错。特别是含非默认构造成员(如 std::unique_ptr、自定义类)时,务必检查移动语义是否完备。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











