返回专辑
·Johan·4 分钟阅读

variant 与访问器:用代数数据类型表达模式

variant 表达传感器消息多态:visit 模式与状态机。

variant 与访问器:用代数数据类型表达模式

1. 非法组合比漏 case 更常见

导航节点用 enum Mode 配 Cloud* c、Scan* s——Idle 时 c 非空、Tracking 时 s 非空,静态分析抓不到,现场偶发 segfault 或错误 publish。代数数据类型把状态与字段绑在一起:std::variant<Idle, Tracking, Lost> 让「Idle 却持有 Cloud*」在类型层难以表达。新增状态时编译器逼改所有 visit,比 switch(enum) 漏 default 可靠。bool + 裸指针在 review 里看起来简单,field 里付的是半夜 gdb。OOP 深继承树把「当前模式能做什么」散在 virtual 函数里,variant 把合法字段与状态绑死,非法组合在编译期或 visit 层就被挡住。现场常见模式是「enum 表示模式,成员变量表示数据」——两套真相,改一处忘另一处;ADT 只有 variant 一个真相源,review 时对照 transition table 即可。

2. 定义状态与字段

Idle、Tracking{Pose, progress}、Lost{since} 组成 NavState。sizeof(variant) 取最大 alternative——点云、图像别直接塞进 variant,用 unique_ptr<Cloud> 包一层。消息多态 variant<Lidar, Imu, Image> 与 ROS 类型并存时,publish 边界 visit 一次转 concrete type,callback 里别反复 get<Image> 赌 index。每个 alternative 用 struct 命名清晰,字段只在该状态下有意义;Idle 不带 progress,Tracking 不带 since,读代码的人不用猜「这个指针现在该不该非空」。

cpp
struct Idle {};
struct Tracking { Pose pose; double progress; };
struct Lost { std::chrono::steady_clock::time_point since; };
using NavState = std::variant<Idle, Tracking, Lost>;

3. visit 与 Overload

on_measurement 里 visit(Overload{...}) 按状态分支转移。读当前态用 get_if<Tracking>,比 get<T> 抛 bad_variant_access 更适合 assert 闸口。非法转移不进 silent no-op,应 log 并切 SafeStop{code};visit 里 throw 可能 half-updated,critical 段 catch 后强制 SafeStop。Overload 继承多个 lambda,C++17 起是 visit 惯用法;团队可封装 match(state, cases...) 减少重复 boilerplate。

cpp
template <class... Ts>
struct Overload : Ts... { using Ts::operator()...; };
template <class... Ts> Overload(Ts...) -> Overload<Ts...>;

4. 转移表与 legacy OOP

维护 (From, Event) → To 表格,visit 实现与表对照 review。legacy State::tick() virtual 机可逐步迁移:新路径 variant,旧路径 interface,边界一次转换。variant 无 vtable、状态 inline,适合固定有限状态集;动态插件加载的状态仍用 factory + interface。迁移策略:新功能只加 variant alternative,旧 enum 路径 deprecated 但不删,直到 transition table 单测覆盖全部边再移除 enum 双轨。

5. 与 expected、ROS 边界

variant 放 exception_ptr 不如 expected<NavState, TransitionError> 可审计。ROS callback 内 state 转移失败应打 log + 进 SafeStop,别 throw 穿越 rclcpp 执行器——半个 variant 更新比全失败更难复盘。holds_alternative 做 debug 断言,生产路径靠 visit exhaustiveness。transition 函数返回 expected<NavState, TransitionError>,caller 决定是重试、告警还是 publish fault topic,比 visit 内 silent return 可观测。

6. 失败症状

get<T> 抛:当前 index 与假设不符。visit 漏 case:仍要 transition 单测覆盖每条边。variant 塞巨型 vector:对象体积膨胀,cache line 浪费。与 enum 并行维护第二套裸指针:两处真相漂移,新增 Mode 只改 enum 不改 visit 时最难查。Recovery 态超时未接 visit 分支,仍保持 Recovery 却 progress 不再更新——表现像卡住但不报错,transition table 单测应抓到这条边。

7. 案例:Recovery 漏接 SafeStop

Recovery 态超时未接 visit 分支,仍保持 Recovery 却 progress 不再更新——表现像卡住但不报错。补 SafeStop 转移后,超时进 Fault{code},日志可 grep,运维可自动复位。transition table 单测抓到这条边,比现场用户截图快一周。事故袋若带 last SafeStop code,可直接对照 table 里哪条 (From, Event) 未实现,而不是猜「是不是又 segfault 了」。

8. 验收

单测:每个 (state, event) 合法与非法转移。压测:状态切换 heap churn 可接受。review:transition table 与代码同步;无裸指针与 enum 双轨。新增 state 类型时编译失败直到 visit 补全——这是 feature 不是 bug。非法转移必须 observable:log + fault code,禁止 silent no-op。property-based 或 table-driven 单测可自动生成 (state,event) 笛卡尔积,漏边在 CI 即红,比人工列 case 更不易漏 Recovery→SafeStop 这类边角。

9. 与录包、运维对齐

事故袋应带 state dump 或至少 last SafeStop code,否则只能猜模式。variant 让非法状态难表示——机器人控制流里模式多、字段不同,ADT 比 OOP 深继承树更贴问题形状。运维复位流程可绑定 Fault code → 允许的操作(清错、重定位、人工介入),与 variant alternative 一一对应,比「重启试试」可审计。从 legacy enum 迁移时,先写 transition 单测再改 visit——测绿后再删裸指针成员,避免半迁移期两种表示并存。variant 不是银弹:状态集无限增长或需运行时插件加载时,仍用 interface + factory;有限、可枚举的控制流才是 ADT 主场。

← 全部文章

johan's blog