
1. 插件边界不能带模板头
感知栈里 Velodyne、Ouster、深度相机各有一套 C++ 类型,主程序与 .so 还可能是不同编译器版本。把 PointCloud<PCL> 写进公共 API,每加一个传感器就重编主程序,RTTI 跨 DSO 也不可靠——typeid 名字字符串比较在不同模块里常对不上。需求是统一投递入口,hot path 不在每个点上虚调用,又能把 bytes 交给对应 decoder,且 ABI 稳定可独立发版插件。类型擦除买的是边界统一,付的是 runtime 间接与分配——层数越少、离 hot path 越远越好。架构评审时应问:擦除层在数据流里出现几次?若每帧三次 make_unique<ErasedModel>,省下的模板编译时间全换成 malloc 峰值。
2. BufferView + type_id registry
擦除层只认指针、长度、类型 id。语义在 host 启动时注册,运行期只读。插件 export C ABI:create_buffer / destroy_buffer / fill_view,主程序不 include 插件 C++ 头。type_id 用 enum class 或稳定整数,别用 typeid(T).hash_code() 跨模块比较。host 侧 0x01 Velodyne、0x02 Ouster,插件只填 id 与 bytes,禁止传模板类型名字符串。registry 启动时一次性注册 decode 函数表,运行期 lookup 是 O(1) 数组索引,不是每点 dynamic_cast。
struct BufferView { const void* data; size_t bytes; uint32_t type_id; };
using DecodeFn = std::expected<Cloud, int>(*)(BufferView);
void registerDecoder(uint32_t id, DecodeFn fn);3. 小对象 type erasure
需要 C++ 侧统一容器时,经典 pimpl + vtable 包装具体类型。擦除有 heap alloc 与间接调用——放在 stage 边界或插件回调,别包在每个 filter 内层循环。每帧多次 make_unique<Model> 会把省下的模板编译时间换成 runtime 分配,profiler 里 malloc 顶满时先数擦除层调用次数,再决定能否下沉到 compile-time 多态。若只有三种固定格式,variant + visit 往往比 any + registry 更省;擦除留给真异构、运行时加载插件的场景。
4. variant、any、function 怎么选
固定 Lidar/Imu/Image 三种时,std::variant<...> + visit 比 std::any 更省更安全,编译期 exhaustiveness。真异构、运行时加载插件才上 any + registry。回调擦除:std::function 易 heap;hot path 用 inplace_function 或模板 + 显式实例化边界。别跨 DSO 传 vector<Eigen::Vector3f> 模板实例——析构器不匹配是 ASan 经典报告,现场表现为插件卸载后随机 crash。选型口诀:编译期已知类型集 → variant;运行时插件 → BufferView + C ABI;回调且 hot → 模板边界实例化。
5. 跨模块契约与版本
registry 与插件版本必须绑定发布物:错 type_id 静默丢帧比 crash 更难查。decoder 失败返回 expected,别抛异常穿越 C 边界。异编译器组合 CI:主程序 GCC、插件 Clang 各 decode 同一录包,输出 hash 一致才允许合入。文档写清 registry 变更是否 breaking——增 id 通常兼容,改 decode 语义不兼容。发布物应捆绑 registry 版本号与插件 so 版本,回滚时二者同步,否则会出现「已回滚仍解码错」的幻觉。
6. 失败症状
any_cast 或错误 id:类型猜错,现场偶发空点云。擦除层过厚:benchmark 输给直接模板路径。热路径 std::function:每 callback 一次 heap。跨 DSO 传 C++ 对象:heap-use-after-free。这些症状在「换编译器发插件」后集中爆发,本地同编译器联调往往测不出来。监控 decode 失败 enum 分布,Truncated 或 VersionMismatch 突增说明链路或版本漂移,比事后 gdb 插件 so 快得多。
7. 案例:演示插件换传感器
演示栈用模板直连 PCL,产品要热插 Ouster 插件——主程序重编两周。改成 BufferView + registry 后,新传感器只发 .so 与 id 常量,主程序二进制不动。回归用同一 bag 对比点云 bbox 与点数,擦除路径延迟在预算内才合入。发布物捆绑 registry 版本号,回滚插件时不回滚 registry 会出现「已回滚仍解码错」的幻觉。新传感器 onboarding 从「改主程序 CMake」变成「注册 id + 发 so」,发版节奏与运维成本都降一截。
8. 验收
插件/主程序异编译器 decode 同一 bag,点云 hash 一致。benchmark:擦除 vs 内联模板,延迟与 alloc 在预算内。新增传感器只增 registry 与新 so。review:hot loop 内无 virtual dispatch per point。监控 decode 失败 enum 分布,Truncated 突增说明链路或版本漂移。CI 跑异编译器 matrix,别只在开发者本机 GCC 联调通过就合入。
9. 与录包、运维对齐
录包应能还原当时 plugin 版本与 type_id 映射,否则事故袋只能猜格式。类型擦除是插件化感知栈的常用折中——统一接口与性能之间的账要在 benchmark 里算清,别在架构图上画完就算完。运维面板可展示当前加载的 plugin 列表与 registry 版本,与 decode 失败率联动告警,比等用户报「点云有时空」再查 so 版本省时间。
相关
也可以看看
johan's blog