该用std::variant而非enum class+struct的核心判断是状态间共享字段少于50%;否则应避免optional堆砌、裸指针和monostate,严格通过std::visit编译期穷举分支并原子赋值切换状态。

直接用 std::variant 写状态机不是“更酷的写法”,而是当状态之间携带的数据结构互不兼容时,唯一能避免 std::bad_variant_access、字段误读、析构遗漏和运行时崩溃的方案。
什么时候该用 std::variant 而不是 enum class + struct
核心判断就一条:看状态间共享字段是否少于 50%。
- Connecting 需要
std::string address和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::variant要帮你避开的坑
std::visit 必须覆盖所有分支,且不能靠 else 补漏
std::visit 的价值不在“分发方便”,而在编译期强制你处理每种状态。漏写一个分支,编译器立刻报错;但新手常因此删掉 else if 改成 else,结果把编译期检查退化成运行时崩溃。
- 必须用
std::decay_t<decltype></decltype>提取真实类型,否则引用类型匹配失败 - 必须用
if constexpr,不用的话,哪怕当前是Connected,编译器仍会尝试编译对Connecting::address的访问,直接报错 - 参数推荐用
const auto&,避免auto&&在含std::string或std::vector的状态里意外触发移动语义 - 更安全的写法是显式列出所有状态,比如用
overloaded{ [](const Idle&) { }, [](const Connecting& s) { /* use s.sock */ } },新增状态时编译器立刻指出缺哪个 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::variant 类型边界的写法,都会让整个设计失效。
- 正确写法:
current_state = Connected{fd, now};—— 自动析构旧状态,就地构造新状态 - 危险写法:
std::get<connecting>(current_state).address = "new";</connecting>—— 一旦当前不是Connecting,直接抛std::bad_variant_access - 若真需要轻量判断类型,改用
std::get_if<connecting>(¤t_state)</connecting>,返回nullptr表示不匹配,安全但需手动判空 - 副作用(如日志、超时重置、socket 关闭)必须封装进
transition函数,避免在多个visit点重复编写清理逻辑
最易被忽略的一点:状态更新必须原子赋值,且所有状态 struct 必须满足 std::is_move_constructible_v;否则赋值可能静默失败或触发未定义行为。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











