std::variant 是类型安全且编译期可验证的有限状态机实现方案:一、用独立轻量 struct 定义各状态(如 Idle、Connecting、Connected、Error),并用 using 声明统一类型;二、通过 std::visit + if constexpr 穷尽分发行为,确保编译期检查与字段直访。

如果您在实现有限状态机时发现各状态需携带结构差异显著的数据,而传统 enum class + 可选字段方案导致逻辑分散、访问前反复判空或运行时崩溃,则 std::variant 是一种类型安全且编译期可验证的替代方案。以下是该方案在实战中的多种应用方法:
一、定义状态类型为独立轻量 struct
每个状态应建模为一个仅含其专属字段的 struct,避免共享字段、预留成员或裸指针,确保数据边界清晰且生命周期由 std::variant 自动管理。这种设计使状态语义直接映射为类型契约,杜绝未初始化访问与误用。
1、为无数据状态定义空 struct:struct Idle {};
2、为需连接信息的状态定义 struct,强制构造时传入必要参数:struct Connecting { std::string address; uint16_t port; int retry_count; };
立即学习“C++免费学习笔记(深入)”;
3、为已建立连接的状态定义 struct,包含 socket 句柄与超时配置:struct Connected { int sockfd; std::chrono::seconds timeout; };
4、为错误状态定义 struct,封装错误码与上下文消息:struct Error { int code; std::string message; };
5、使用 using 声明统一状态类型,顺序按典型流转排列便于阅读:using State = std::variant
二、使用 std::visit 安全分发状态行为
std::visit 是访问 std::variant 中当前状态的唯一安全方式,它强制编译期穷尽所有可能类型分支;漏写任一状态将直接导致编译失败,从而提前暴露逻辑缺失,而非等到运行时抛出 std::bad_variant_access。
1、采用泛型 lambda 配合 if constexpr 与 std::decay_t 提取真实类型:std::visit([](const auto& s) { using T = std::decay_t
2、对每个状态类型编写独立的 if constexpr 分支,禁止使用普通 if 或 else:if constexpr (std::is_same_v
3、在 Connecting 分支中直接访问其字段,无需判空或断言:else if constexpr (std::is_same_v
4、在 Connected 分支中使用其专属资源字段:else if constexpr (std::is_same_v
5、在 Error 分支中完整访问错误上下文:else if constexpr (std::is_same_v
三、封装原子状态转换函数
状态变更不应通过字段级修改或 std::get 强制取值完成,而应通过构造新状态对象并整体赋值实现。此举确保旧状态自动析构、新状态完全初始化,避免中间不一致或资源泄漏。
1、定义 transition 函数,接收当前状态与事件参数,返回新状态:State transition(const State& current, const ConnectEvent& e) { return Connecting{e.host, e.port, 0}; }
组合式C++代码评审方案,融合静态分析、AI推理、多轮迭代评审和C++专项检查,适用于PR审查、增量代码审查、全项目评审和代码质量评分,触发词包括review cpp、cpp代码评审、C++review、代码审查。
2、在事件响应逻辑中执行原子赋值:state = transition(state, connect_event);
3、若需复用旧状态部分数据(如从 Connecting 进入 Connected 时保留 socket),应在构造新状态时显式传入:state = Connected{old_sockfd, std::chrono::seconds{30}};
4、禁止对 std::get
5、避免使用 std::get_if 后手动赋值,因其无法保证析构时机可控:// 不推荐:state = std::get_if
四、规避常见陷阱与硬约束
违反以下约束将破坏 std::variant 的核心优势,引发运行时崩溃、内存泄漏或编译期不可维护性。这些规则是类型安全状态机的底线要求,不可妥协。
1、禁止在状态 struct 中使用 std::optional 成员:// 错误:Connecting{std::optional<:string> host} 削弱构造强制性
2、禁止使用裸指针(如 int* 或 char*)作为状态成员:// 危险:std::variant
3、空状态必须使用 struct Idle {};,而非 std::monostate:// 推荐:语义明确,支持后续扩展字段而不破坏二进制兼容性
4、所有状态 struct 必须满足平凡可析构或资源自动管理:// 正确:Connected 包含 std::unique_ptr 或 std::string,std::variant 会正确调用其析构函数
5、状态类型列表中不得出现重复类型:// 编译错误:std::variant
五、使用辅助模板显式覆盖所有分支
相比泛型 lambda,使用 overloaded 辅助模板显式列出每个状态类型的处理函数,能进一步防止因 if constexpr 分支遗漏或误写 else 而退化为运行时逻辑漏洞。编译器将在新增状态后立即报错指出缺失处理项。
1、定义 overloaded 模板(标准库未提供,需自行实现约 5 行代码):template
2、特化 overloaded 实例,为每个状态类型提供独立 lambda:overloaded{ [](const Idle&) { /* idle logic */ }, [](const Connecting& s) { /* use s.address, s.port */ }, [](const Connected& s) { /* use s.sockfd */ }, [](const Error& e) { /* use e.code */ } }
3、将该实例传入 std::visit 调用:std::visit(overloaded{ /* 如上 */ }, state);
4、新增状态类型后,编译器将提示“no matching operator()”并标出缺失的 lambda,强制补全:// 新增 Reconnecting 状态后,必须添加 [](const Reconnecting& r) { ... }
5、每个 lambda 参数必须声明为 const T&,避免因 auto&& 触发意外移动语义导致资源提前释放:// 正确签名:[](const Connecting& s),而非 [](auto&& s)


















