跳转到主内容
新派编程网 - 新潮编程理念,全新技术课堂!

C++ std::variant 在有限状态机编程中的应用实战 _ 现代技巧【实战】

应仅在状态携带异构、专属、不可共享数据时用std::variant实现FSM;纯枚举跳转反增开销。核心判据是状态是否需类型互不兼容的字段,如ConnectingState含string/int、ConnectedState含sockfd/timeout、DisconnectedState仅error_code,此时variant可避免optional堆砌与运行时判空,编译期强制合法访问。 直接用
std::variant
实现有限状态机(FSM)是可行的,但只在状态携带**异构、专属、不可共享**的数据时才真正值得——纯枚举跳转(如
Idle
/
Running
/
Done
三个空状态)反而引入冗余和编译开销。 什么时候该用
std::variant
而不是
enum class
核心判断标准:状态是否需要附带类型互不兼容的字段。 ConnectingState 需要
std::string address
和
int retry_count
ConnectedState 需要
int sockfd
和
std::chrono::seconds timeout
DisconnectedState 只需
int error_code
此时若硬塞进一个
struct
+
enum class
,就得堆一堆
std::optional
字段,每次访问前必须判空、易漏初始化、字段语义模糊。而
std::variant
让编译器强制你“只处理当前合法状态及其全部字段”,非法访问在编译期就报错。
std::visit
的 lambda 必须配合
if constexpr
和
std::decay_t
写
std::visit([](auto& s) { ... }, state)
是常见起点,但内部若用普通
if
或
typeid
,就等于放弃
std::variant
的核心优势。 立即学习 “ C++免费学习笔记(深入) ”; 漏掉
std::decay_t
→
decltype(s)
是引用类型,
std::is_same_v
永远为 false 不用
if constexpr
→ 所有分支代码都参与模板实例化,哪怕
s
是
ConnectedState
,编译器仍会尝试编译
s.address
,直接报错 正确写法必须是:
using T = std::decay_t; if constexpr (std::is_same_v) { ... }
状态转换不能靠裸赋值,得封装
transition
函数 直接写
state = Connecting{host, port}
看似简单,但副作用(如日志、资源申请、前置校验)会散落在各处,难以统一维护。 把状态变更逻辑收口到一个
transition
函数里,例如
transition(state, Connect{host, port})
函数内部可基于当前状态做合法性检查(比如禁止从
ErrorState
直接跳到
ConnectedState
) 副作用(如 socket 创建、重试计数重置)集中在此处,避免重复或遗漏 避免依赖
index()
或
holds_alternative()
做主逻辑分支——它们适合调试或日志,不适合驱动状态流转 容易被忽略的细节:默认构造、大对象与空状态语义
std::variant
默认构造会调用第一个类型的默认构造函数,这常被误当作“初始状态”的设计依据,但实际隐患不少。 别把
std::monostate
塞在
using State = std::variant
开头,除非你真需要“无状态”作为合法初值;否则它掩盖了状态建模不完整的问题 状态 struct 中若含大对象(如
std::vector
),优先包一层
std::unique_ptr
,避免栈空间暴涨 每个状态 struct 应只放其“必要且充分”的字段,不预留、不共享、不妥协;
Idle{}
比
struct Idle {}
更清晰,后续加字段也自然 所有状态类型必须满足
std::is_move_constructible_v
,否则
std::variant
赋值可能失败

相关文章