该用 std::variant 而非 enum class + struct 的情形是:各状态携带数据类型与数量差异大,且共享字段少于 50%;此时 variant 通过类型安全、编译期检查和原子状态更新保障正确性与可维护性。

用 std::variant 写有限状态机,只在状态携带**互不兼容、共享字段少于 50%** 的数据时才真正值得;否则用 enum class + 单一 struct 更轻、更安全、更易维护。
什么时候该用 std::variant 而不是 enum class
核心看字段是否“按状态分裂”:如果每个状态要带的数据类型和数量差异大,且共享字段少于 50%,std::variant 才发挥价值。
-
ConnectingState需要std::string address和int retry_count -
ConnectedState需要int sockfd和std::chrono::seconds timeout -
IdleState真的什么也不需要 —— 定义为struct IdleState {};
硬塞进一个 struct FSMData + enum class State,就得堆一堆 std::optional 和 std::optional<:string>,访问前必须反复检查 has_value();而 std::variant 让类型系统强制你“每次只处理一种合法状态及其全部字段”,编译期就卡死非法访问。
std::visit 必须显式覆盖所有状态分支
std::visit 的核心价值不是“方便分发”,而是让漏写某个状态分支成为编译错误。泛型 lambda 配合 if constexpr 看似简洁,但新手常因编译失败删掉分支、补个 else 了事,结果又回到运行时不可控。
- 推荐写法是显式列出所有状态,例如用
overloaded{ [](const IdleState&) { /* do nothing */ }, [](const ConnectingState& s) { std::cout - 每个 lambda 参数必须为
const T&(非auto&&),否则if constexpr在部分特化下可能失效 - 新增状态时,编译器会报错指出哪个 lambda 缺失 —— 这才是你该依赖的“强制审查机制”
状态 struct 设计的三个硬约束
每个状态 struct 不是容器,而是该状态的“数据契约”。违反任一约束,就会引入字段误访问、默认构造无意义或生命周期失控等隐患。
- 禁止出现
std::optional或裸指针:比如ConnectingState里不该有std::optional,而应强制通过构造函数传入 ——sock_fd ConnectingState(int fd, std::string ip) - 空状态必须定义为
struct IdleState {};,别用std::monostate:后者语义模糊,后续加bool was_recently_active会破坏二进制兼容性 - 禁止在
std::variant里放std::unique_ptr或* T*:这等于放弃自动析构优势,资源管理责任被踢给上层,极易泄漏
状态更新必须原子赋值,绝不能原地修改
状态变更必须以完整新状态对象一次性赋值给 std::variant 变量,不可分步修改内部字段。这能防止因异常或并发导致 std::variant 处于非法构造状态,同时保障每个状态 struct 的构造函数被正确调用。
- 获取新状态所需全部参数,例如 socket 句柄与 IP 地址
- 构造目标状态实例:
auto next = ConnectingState{fd, "192.168.1.100"}; - 执行原子赋值:
current_state = std::move(next); - 禁止使用
std::get去原地修改字段 —— 这绕过了(current_state).address = "new" std::variant的类型安全与生命周期管理
最易被忽略的一点是:状态更新的原子性不是可选项,它是避免中间不一致、析构遗漏和运行时崩溃的底线。一旦开始手动改字段,就等于退回到裸 union + 手动生命周期管理的老路。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











