std::variant 适合状态携带不同数据且无需虚函数或动态内存的中等复杂度状态机;它避免 enum class + struct 的字段冗余和类型不安全问题,强制编译期类型检查,推荐用 std::visit 分发处理各状态。

直接用 std::variant 实现状态机,适合状态携带不同数据、且你不想写虚函数或管理动态内存的场景。它不是万能替代,但对中等复杂度、类型边界清晰的状态流转非常高效。
为什么不用 enum class + switch?
当你发现每个状态要附带的数据结构差异很大时,enum class State { Idle, Connecting, Connected }; 就开始露怯了:Idle 不需要任何字段,Connecting 要存 std::string host 和 int port,Connected 又得带一个 int socket_fd —— 你只能把所有字段塞进一个大 struct,靠状态枚举判别哪些字段有效,或者反复 reinterpret_cast,极易出错。
而 std::variant 让每个状态只带自己真正需要的字段,编译器强制你处理每种情况,不遗漏、不误读。
常见错误现象:std::get<connecting>(state)</connecting> 在 state 实际是 Idle 时抛 std::bad_variant_access;用 std::holds_alternative 判定后仍手动 cast,绕过类型安全。
立即学习“C++免费学习笔记(深入)”;
- 别用
std::get<t></t>直接强取,除非你 100% 确认当前类型(比如刚赋值完) - 优先用
std::visit统一调度,避免分支遗漏 - 若需在多个地方访问同一状态字段,把提取逻辑封装进辅助函数,不要散落各处
如何定义状态类型并保证可访问性
每个状态应是一个轻量 struct,仅含该状态专属字段,不继承、不虚函数、不裸指针:
struct Idle {};
struct Connecting {
std::string host;
uint16_t port;
};
struct Connected {
int socket_fd;
std::chrono::steady_clock::time_point last_active;
};
using State = std::variant<Idle, Connecting, Connected>;
关键点:
- 所有状态类型必须可构造、可析构、可移动(满足
std::is_move_constructible_v) - 避免重复类型(如
std::variant<int int></int>编译失败),必要时用空 tag struct 占位 - 默认构造
State会初始化为Idle{}(因Idle是第一个类型),这通常符合语义 - 若需“无状态”占位,可用
std::monostate放在首位,但要慎用——它容易掩盖逻辑缺陷
状态转换与 visit 的正确写法
std::visit 不是语法糖,它是类型分发的唯一安全出口。写错 lambda 签名或漏覆盖分支,编译就报错,这是优势,不是负担。
例如实现一个 onConnectSuccess() 函数,只对 Connecting 和 Idle 做响应:
void onConnectSuccess(State& s) {
std::visit([](auto&& state) -> void {
using T = std::decay_t<decltype(state)>;
if constexpr (std::is_same_v<T, Connecting>) {
// 成功后切到 Connected
state = Connected{ /* fd from system */ };
} else if constexpr (std::is_same_v<T, Idle>) {
// 从空闲态发起连接
state = Connecting{"api.example.com", 443};
}
// 其他状态(如 Connected)什么也不做,不写 else 分支
}, s);
}
注意:
- 必须用
if constexpr,不能用普通if—— 否则未实例化的分支里调用不存在的成员会编译失败 - lambda 返回类型显式写
-> void,避免推导歧义 - 状态变更用
state = NewType{...},不是s = NewType{...}——state是当前变体中那个类型的引用,赋值即替换整个 variant - 不要在
std::visit外部做类型判断再分支,那等于放弃编译期检查
容易被忽略的生命周期与异常安全细节
std::variant 内部会自动调用所含类型的构造/析构函数,但前提是这些类型本身异常安全。如果某个状态 struct 的构造函数可能抛异常(比如 Connecting 构造时解析 DNS 失败),那么 state = Connecting{...} 就可能中途失败,导致 state 进入未定义状态(实际是保持原值,但标准不保证)。
实操建议:
- 状态 struct 应尽量保持 trivial 或至少是
noexcept构造;耗时/可能失败的操作(如网络连接)应放在状态外完成,再以结果驱动状态切换 - 不要在状态 struct 的析构函数里触发新状态切换(比如
~Connected() { context->setState(Idle{}); }),此时 variant 正在销毁中,行为未定义 - 若需日志或副作用,统一在
std::visit的 lambda 里处理,而不是分散到各个状态的成员函数中 - 调试时用
s.index()查看当前类型序号,比typeid(state).name()更可靠(后者依赖 RTTI,且名字不可移植)



















