std::variant 适合状态携带不同数据且无需动态增删的场景,它通过零开销、类型安全的方式替代虚函数和堆分配,避免 enum+union 的生命周期与类型安全问题。

直接用 std::variant 实现状态机,适合状态携带不同数据、且不需运行时动态增删状态类型的场景——它不是“替代状态模式”,而是用零开销的类型安全方案,绕过虚函数和堆分配。
为什么不用 enum class + switch 处理异构状态?
当每个状态要带专属字段(比如 Connecting 存 socket 句柄、Authenticating 存 token 字符串、Connected 存 session ID 和心跳计时器),用单一 enum class State 就得手动管理一堆可选字段、频繁判空、反复 reinterpret_cast 或 union hack。容易漏初始化、访问未构造字段、或在析构时调用错误类型的 dtor。
实操建议:
- 别在 struct 里塞
std::optional<socket_t></socket_t>+std::optional<:string></:string>+ … —— 内存浪费、逻辑分散、默认构造无意义 - 别用裸
union手动管理生命周期 —— 析构时机难控,C++17 之后已无必要 - 如果状态间共享字段超过 50%,才考虑基类抽取;否则,让每个状态 struct 只放它真正需要的成员
如何定义状态 variant 并保证编译期安全?
核心是把每个状态建模为独立 struct,再用 std::variant 统一封装。编译器会强制你覆盖所有分支,避免漏处理某类状态。
实操建议:
- 用
using State = std::variant<idle connecting connected error></idle>,每个 struct 定义在头文件中,保持轻量 - 每个状态 struct 的构造函数只接收其必需参数,例如
Connecting(socket_t s, std::string ip),不接受“通用配置对象” - 禁止在 variant 中放入指针类型(如
std::variant<a b></a>)—— 这会把内存管理责任推给上层,违背 variant 的自动析构优势 - 若某个状态暂无数据(如
Idle),也定义为空 struct:struct Idle {};,而非用std::monostate—— 后者语义模糊,不利于后期扩展动作字段
怎么用 std::visit 正确分发状态行为?
std::visit 不是语法糖,它是编译期强制穷举的开关。写错一个 lambda 分支,编译就失败,比运行时报 std::bad_variant_access 更早暴露问题。
实操建议:
- lambda 必须是泛型或显式列出所有类型,例如:
std::visit([](const auto& s) { /* 处理所有 */ }, state),或更安全的显式写法:std::visit(overloaded{ [](const Idle&) { /* ... */ }, [](const Connecting& s) { /* use s.sock */ }, ... }, state) - 不要在 visit 里做状态变更(如
state = Connected{...})——std::visit是只读访问,状态更新应由外部逻辑统一控制 - entry/exit 动作不能靠 visit 自动触发;需在每次赋值前/后显式调用钩子函数,例如:
on_exit(current_state); state = Connected{...}; on_enter(state); - 若需从 visit 内部返回不同类型的值(如
std::optional<int></int>),确保所有分支返回相同类型,否则编译失败
容易被忽略的资源生命周期陷阱
std::variant 确保每个活跃状态的析构函数被调用,但它不保证状态切换时的资源迁移是原子的。比如从 Connecting 切到 Connected,socket 句柄必须转移而非复制,否则原状态析构时 close 掉合法句柄,新状态拿到的是已失效句柄。
实操建议:
- 状态 struct 中含非 trivial 资源(socket、file handle、thread)时,必须定义移动构造/赋值,且禁用拷贝(
= delete) - 避免在状态 struct 中持有
std::shared_ptr<context></context>—— 容易和 Context 形成循环引用;改用Context&引用或std::weak_ptr(仅当真需异步回调) - 调试时加断点观察
state.index()变化,比打印枚举名更可靠 —— 因为 index 是std::variant内部唯一确定的活跃标识 - 上线前用
static_assert(std::is_nothrow_move_constructible_v<state>)</state>检查,防止状态切换因异常中断导致中间态残留
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











