std::variant适用于状态间共享字段少于50%的场景,否则应使用enum class+统一struct;必须用if constexpr和显式分支处理visit,状态struct需满足raii、无optional/裸指针、空状态用struct idle{}等硬约束。

std::variant 不是状态机的“高级写法”,而是当状态携带互不兼容数据时,唯一能避免 std::bad_variant_access、字段误访问和运行时崩溃的方案。硬套在纯枚举跳转场景里,只会增加编译负担、降低可读性。
什么时候该用 std::variant 而不是 enum class
只看一个数字:状态之间共享字段是否少于 50%。
- Connecting 需要
std::string ip和int retry_count;Connected 需要int sockfd和std::chrono::seconds timeout;Idle 真的什么也不需要 → 适合std::variant - 所有状态都带
std::string id、uint64_t timestamp、bool is_active→ 别用std::variant,老实用enum class+ 单一struct - 用
std::optional塞满一个大struct是典型反模式:访问前必须反复判空、默认构造无意义、内存浪费,且新增状态时极易漏初始化
std::visit 必须覆盖所有分支,且不能靠 else 补漏
漏写一个状态分支,编译器立刻报错——这不是缺陷,是保护机制。但新手常因此删掉 else if 改成 else,结果把编译期检查退化成运行时崩溃。
- 必须用
std::decay_t<decltype></decltype>提取真实类型,否则引用类型不匹配,std::is_same_v永远为 false - 必须用
if constexpr,不用的话,哪怕当前s是Connected,编译器仍会尝试编译对Connecting::ip的访问,直接报错 - 参数推荐写成
const auto& s,避免auto&&在含std::string或std::vector的状态里意外触发移动语义 - 更安全的写法是显式列出所有状态,比如
overloaded{ [](const Idle&) { }, [](const Connecting& s) { /* use s.ip */ } };新增状态时编译器立刻指出缺哪个 lambda
状态 struct 设计的三个硬约束
每个状态 struct 不是容器,而是该状态的“数据契约”。违反任一约束,就会引入字段误访问、析构遗漏或生命周期失控。
- 禁止出现
std::optional或裸指针:比如Connecting里不该有std::optional<int> sock_fd</int>,而应强制通过构造函数传入 ——Connecting(int fd, std::string ip) - 空状态必须定义为
struct Idle {};,别用std::monostate:后者语义模糊,后续加bool was_recently_active会破坏二进制兼容性 - 禁止在
std::variant里放std::unique_ptr<t>*</t>或T*:这等于放弃自动析构,资源管理责任被踢给上层,极易泄漏
状态变更必须原子、完整
状态更新不能只改部分字段,也不能跨状态复用字段。原地修改字段(如 std::get<connecting>(state).ip = "new"</connecting>)绕过了构造/析构逻辑,一旦异常中途发生,variant 可能处于非法构造状态。
- 获取新状态所需全部参数,例如 socket 句柄与 IP 地址
- 构造目标状态实例:
auto next = Connecting{fd, "192.168.1.100"} - 执行原子赋值:
state = std::move(next) - 确认原状态的析构函数已在赋值前自动调用 —— 这依赖每个状态
struct的字段都是 RAII 类型(如std::string、std::unique_ptr),而非裸指针或FILE*
最易被忽略的一点:状态更新的原子性依赖于每个字段的析构是 noexcept 的。如果某个状态 struct 里塞了可能抛异常的非平凡析构函数,variant 赋值过程就可能中途终止,留下未定义行为。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











