
1. viz 插件 clear 了 tracker 缓存
公开 vector& poses() 非 const,viz 插件 poses.clear() 破坏 invariant——本应只读的路径修改了共享 map,多线程 race 难查。const 不是「只读装饰」,是接口契约:调用方知道你不会改 buffer,编译器也能优化。process(const Cloud&) 与 process(Cloud&) 差的是能不能 alias 外部内存、能不能 in-place 滤波——语义差一个字,field 差一整条轨迹,且 bug 在插件侧,主节点日志里什么都没有。注释写「请勿修改」拦不住第三方插件,const 才让编译器 enforce;robotics 栈里插件与 viz 众多,const 传播是防 footgun 最便宜的手段,应在 API review 与 CI tidy 里强制执行。
2. 参数 const 传播
class Localizer {
public:
void setMap(const OccupancyGrid& map);
Pose estimate(const Scan& scan) const;
private:
std::shared_ptr<const OccupancyGrid> map_;
};成员函数标 const 才能被 const Localizer& 调用——多线程读 snapshot 时常用 const 对象加 mutex 保护内部 mutable 缓存(谨慎)。对外 API process(const PointCloud&) 强制只读;内部 copy-on-write 才 mutate。setMap 保存 shared_ptr<const> 表达地图快照不可变,别保存裸指针后 assume 外部不改。estimate const 向调用方承诺「本调用不 mutating 地图」,与 setMap 非 const 形成清晰读写分界。
3. 返回类型与容器
返回 const vector& 或 span<const Pose>,别返回 mutable 引用。for (auto& p : poses) 若容器非 const,调用方能 mutate——对外只读视图,内部 mutating pipeline 用 private 成员加明确 insertPose()。getLatestScan() const 返回 shared_ptr<const Scan> 比 mutable 引用更能表达 snapshot 语义——caller 不会意外改 buffer,DDS 也不会 alias 半更新状态。公开 mutating API 用动词 insert/update。只读 getter 若返回非 const 引用,等于把 invariant 维护责任散播给所有插件——viz、录包、诊断皆可能成为 mutator,const 传播是防这类 footgun 最便宜的手段。
4. mutable 缓存
昂贵预处理(distance field)可 mutable 加内部 mutex,对外仍 const 接口——文档写清 thread-safety 与 invalidate 条件;map 更新后必须 bump generation,否则 const lookup() 返回 stale TF。
Transform getTf() const {
std::scoped_lock lk(mutable_mu_);
if (!cache_) cache_ = lookupTf();
return *cache_;
}const 函数里 const_cast 改 shared state 是 smell——TSan 与读者都会被坑。getTransform() const 却改 observable state 无文档,review 应打回。mutable 缓存是「逻辑 const」而非「物理 const」——调用方仍应把 const 方法当读接口,invalidate 条件必须可核对。
5. const 与指针
const uint8_t* data:指针可变、数据不可变。uint8_t* const data:指针不可变。API Review 看 T* 是否应 const T*——Eigen Map<const Vector3d> 表达只读视图。const string& vs string_view:后者不保证 null-terminated,别直接 %s 给 C API。constexpr 标定常数放 header,const 运行时配置放 struct。多线程读路径若返回非 const 引用,等于把写权限散播到所有 caller——viz、录包、诊断插件皆可能成为「意外 mutator」。
6. 编译器与静态分析
启用 clang-tidy readability-const-return-type。Code review:每个非 const 引用参数问「为何必须写」。去掉 const 后编译通过但语义变——in-place 修改 caller buffer 的路径要有测试覆盖。const 正确性是一次性成本——后期改接口全树重编更贵。团队可在 CI 开 const 相关 tidy,把「公开 mutable 引用」当成 merge blocker,与 mutex 的 LOCK_ORDER 同级对待。
7. 案例:public poses() 被 viz 清空
某 tracker 公开 mutable vector& poses() 给 viz 画轨迹,插件升级后某帧 clear() 缓存,定位偶发丢目标——主栈无 log。改成 span<const Pose> 加 insertPose() 后消失。根因是只读契约缺失,不是 viz 恶意。教训:只读返回 span/const&,mutating 用动词 API 名。第三方插件不可控,const 是主栈唯一能靠编译器 enforce 的边界;注释「请勿 clear」拦不住升级后的插件行为。这类 bug 应在插件 fuzz 测试里覆盖:只读 API 不可 mutate 内部容器。
8. 验收
- 只读接口 span/const&,mutating 动词 API。
- mutable 缓存配 mutex 并文档 thread-safe。
- 插件/viz 路径 fuzz:只读 API 不可 mutate 内部容器。
- CI 开 const 相关 clang-tidy。
- 每个非 const 引用参数在 review 有「必须写」理由,缺则打回。
契约写进类型比注释更硬——const 是接口的一部分,不是风格偏好;后期为插件擦屁股改接口,全树重编成本更高。只读与 mutating 的分界应出现在类型系统里,而不是 wiki 里的「请勿修改」。CI 开 const 相关 tidy 可与 LOCK_ORDER 同级作为 merge blocker;estimate const 与 setMap 非 const 形成清晰读写分界,调用方一眼可知会否 mutating,API review 应强制执行,与 LOCK_ORDER 同级。
相关
也可以看看
johan's blog