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