
1. 改一行内部头,CI 全树四十分钟
头文件是 C++ 项目的「公共 API 面」:改一行 #include 顺序或暴露一个 std::vector 成员,全树重编是小事,ABI 裂隙才是大坑。机器人仓库里感知、控制、驱动三条线并行,头文件卫生直接决定能不能把算法 .so 交给现场升级。
上次 plugin 升级后 nm -D 仍干净,但公开头多了 #include <optional>——下游全量重编 18 分钟。头文件不是「把实现写进去方便」的地方——它是契约。契约越薄,迭代越快。12 个公开头去掉 PCL transitive include 后,全量编译从 11 分钟降到 7 分钟,dynamic symbol 从 2k 降到 200。
2. 编译防火墙:Pimpl 与前置声明
头文件里能前置声明就不 #include。实现细节进 .cpp。调用方编译单元看不到 pcl::PointCloud、Eigen 类型,改内部实现不触发全量重编。std::unique_ptr<Impl> 需要 .cpp 里显式析构定义,否则 incomplete type 报错。
感知节点头文件里 #include "lidar_decoder.hpp" 会把 PCL、OpenCV transitive include 拉进每个 .cpp——改成 class LidarDecoder; + std::unique_ptr<LidarDecoder>,实现细节留在 .cpp,全树 -j 编译时间通常立刻可见下降。
// slam_backend.h — 对外稳定
#pragma once
#include <memory>
class SlamBackend {
public:
SlamBackend();
~SlamBackend();
void process(const uint8_t* cloud, size_t nbytes);
private:
struct Impl;
std::unique_ptr<Impl> impl_;
};3. INTERFACE 与 PRIVATE 链接 discipline
CMake 里 target_link_libraries(my_node PUBLIC pcl_common) 会把 PCL 头泄露给所有 downstream。改 PRIVATE,只把自有 include/ 标 PUBLIC。配合 CMAKE_EXPORT_COMPILE_COMMANDS 给 clangd,新人改错 include 时 IDE 比 CI 更早红。
公开头里 #include <memory> 可以,#include <Eigen/Dense> 不行——Eigen 实例化会穿过 API 面。需要 Eigen 时在 .cpp 里 include,公开头只前向声明或固定大小 double[16]。把 12 个公开头里的 PCL include 改成前向声明后,全量编译从 11m 到 7m(GCC 11, 32 核)。
4. 内联与模板边界
header-only 模板库方便分发,但任何符号变化都迫使所有 TU 重编。若模板只在 .cpp 内实例化(显式 instantiation),对外只暴露非模板 C API,编译防火墙才真正生效。include 顺序约定:对应 .h → C 标准库 → 第三方 → 项目内。
禁止「万能预编译头」掩盖循环依赖——A.h 包含 B.h、B.h 又包含 A.h,最后靠 forward declare 硬撑,改一处全炸。detail:: 或 internal/ 目录约定:代码审查看到 internal 头在公开头出现可直接打回。依赖应单向:驱动 → 算法 → 工具,不能反向。
5. ABI 相关禁区
公开头里避免:非 POD 结构体按值传递跨 .so、带虚函数的类布局变更、enum underlying type 未固定。Linux 默认 -fvisibility=hidden,只 export 必要符号。公开 enum 固定 underlying type。插件 API 禁止 std::string 按值——用 char buffer + len。
不同 TU 看到不同 struct 大小——缺少对齐不一致,是 field 上静默数值错的根源。链接出的 libslam.so dynamic symbol 越少,插件边界 audit 一眼能看完 export 列表。
6. 验收
- 改 Impl 成员后,期望只重编对应
.cpp及直接依赖。 - 插件升级后主程序未重编,偶发
double free——STL 容器跨边界。 - clang-tidy include-cleaner CI 可选;Pimpl destructor 必须在
.cpp定义。 - IWYU 报告写进 PR,reviewer 只盯
my_pkg/include/diff。
include-what-you-use -Xiwyu --max_line_length=120 slam_node.cpp
nm -D build/libslam.so | c++filt | rg 'pcl::' && exit 1 || true7. 案例:PCL include 泄漏导致全树重编
12 个公开头里的 PCL include 改成前向声明后,每个 TU preprocess 行数平均少 1.1 万行,全量编译从 11m 到 7m。更关键的是 dynamic symbol 从 2k 降到 200,插件边界 audit 一眼能看完 export 列表。公开头禁止 transitive PCL/OpenCV;Eigen 留在 .cpp。IWYU 报告写进 PR,reviewer 只盯 include 目录 diff。
8. 与插件边界对齐
插件 API 审查 checklist:无 STL 容器、无异常跨边界、无 Eigen 类型出现在公开头。Pimpl destructor 必须在 .cpp 定义,否则 incomplete type 报错。头文件是契约——契约越薄,迭代越快,field 热插拔越安全。
9. 决策:include 能不能进公开头
三个问题:下游 TU 是否需要这个类型的完整定义?能否前向声明 + Pimpl 替代?链接能否改 PRIVATE 避免 transitive 泄漏?若三个答案都是「不需要/能/能」,include 不该出现在公开头。IWYU 和 include-cleaner 是辅助,最终判断靠 reviewer 盯 include 目录 diff。每次公开头新增 include 都是 ABI 和 compile time 的双重风险。头文件是契约,契约越薄迭代越快。
相关
也可以看看
johan's blog