std::variant无法实现编译期状态机,因其所有访问操作均为运行时行为;真正的编译期状态机需用模板参数表示状态、特化trait定义转移规则,并通过static_assert在实例化时静态校验。

std::variant 本身不支持编译期状态转移
直接用 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 - 状态机主体是类模板,携带当前状态类型作为模板参数:
templatestruct StateMachine {};
此时所有状态切换都发生在模板实例化阶段,没有运行时 std::variant 的开销或歧义。
如果非要结合 std::variant,只能用于运行时桥接层
可以将编译期状态机封装后,对外暴露一个运行时接口,内部用 std::variant 存储「当前状态的运行时视图」——但这只是包装,不是编译期实现本身。容易踩的坑包括:
立即学习“C++免费学习笔记(深入)”;
- 误以为
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 个,维护成本会陡增。











