std::variant无法实现编译期状态机,因其所有访问操作均为运行时行为;真正的编译期状态机需用模板参数表示状态、特化trait定义转移规则,并通过static_assert在实例化时静态校验。
直接用 std::variant 实现「编译期状态机」是个常见误解。它本质是运行时类型擦除容器,std::variant 的 index()、valueless_by_exception()、甚至 std::get_if 都是运行时行为。即使配合 constexpr 构造,也无法在编译期做分支决策(比如根据当前状态决定下一个状态类型)。
核心思路是把「状态」作为模板参数列表,把「转移规则」编码为 SFINAE 或 constexpr if + 类型 trait,让编译器在实例化时静态选择路径。例如:
struct Idle {};、struct Running {};
template struct can_transition : std::false_type {}; ,再对合法组合显式特化为 std::true_type
template struct StateMachine {};
此时所有状态切换都发生在模板实例化阶段,没有运行时 std::variant 的开销或歧义。
可以将编译期状态机封装后,对外暴露一个运行时接口,内部用 std::variant 存储「当前状态的运行时视图」——但这只是包装,不是编译期实现本身。容易踩的坑包括:
std::visit([](T&&) { ... }, v) 是编译期分发:实际是运行时根据 v.index() 调用对应分支constexpr 函数里对 std::variant 做 std::get:C++20 起仅当 v 是 constexpr 且持有 T 时才允许,但无法泛化判断std::monostate 和「无状态」:它只是占位符,不参与编译期逻辑推导下面这个例子不依赖 std::variant,但能静态检查非法转移,并在编译失败时给出清晰错误位置:
#includestruct Idle {}; struct Running {}; struct Paused {}; template struct can_transition : std::false_type {}; template<> struct can_transition : std::true_type {}; template<> struct can_transition : std::true_type {}; template<> struct can_transition : std::true_type {}; template<> struct can_transition : std::true_type {}; template struct StateMachine { template constexpr auto transition() const { static_assert(can_transition ::value, "Illegal state transition"); return StateMachine {}; } }; // 使用: // auto sm = StateMachine {}.transition (); // OK // auto bad = StateMachine {}.transition
(); // 编译失败
真正的难点不在语法,而在于如何把业务中的「事件」也建模为类型,并与状态组成二维转移表——那部分需要大量 trait 拆解和别名模板辅助,且一旦状态数超过 5–6 个,维护成本会陡增。