clang++ -M 生成依赖文件必须同时指定 -MF、-MT 和 -M 参数,否则 .d 文件无法被 Make 或 Ninja 正确读取;路径需统一基准,避免依赖错位;-MM 会剔除系统头导致依赖失真,应保留并精准过滤;Makefile 中 -include 必须放在规则之后且加短横线。

clang++ -M 生成依赖文件必须加 -MF 和 -MT
直接跑 clang++ -M main.cpp 只会把依赖刷到终端,没法存下来供 Make 或 Ninja 消费。关键不是“能不能出依赖”,而是“能不能被构建系统读进去”。
必须配齐这三组参数:
-
-M:启用依赖生成(只扫描#include,不展开宏) -
-MF deps/main.d:指定输出文件路径,否则依赖信息直接丢弃 -
-MT "obj/main.o":显式声明目标名,否则默认是main.o:,和 Makefile 里写的obj/main.o:对不上,导致规则失效
漏掉任一参数,.d 文件就不可用。比如忘了 -MT,Make 会找不到匹配的目标,干脆跳过该依赖更新。
多文件项目要统一路径前缀,否则依赖图拼不起来
对每个 .cpp 单独跑 clang++ -M 时,原始输出里的路径默认是绝对路径或相对于当前工作目录的路径。如果构建在不同子目录下执行,或者 make -C build 切了路径,.d 文件里记录的 src/a.h 和 ../include/b.h 就会错位。
解决方法是强制统一基准:
- 编译前用
cd /path/to/project && clang++ -M -MF deps/a.d -MT "obj/a.o" src/a.cpp - 或用
-I配合-fmacro-prefix-map(Clang 10+)把绝对路径映射为相对路径:-fmacro-prefix-map=/full/path/to/project=. - 后续清洗时再用
sed 's|^\.\/||'去掉开头的./,确保所有路径从项目根开始
不统一路径,后续用 include-what-you-use 或 Graphviz 画图时,同一个头文件可能被识别成十几个不同节点。
别信 -MM,它删掉系统头反而让依赖分析失真
-MM 确实能排除 /usr/include 下的系统头,看起来“干净”——但这是假干净。真实编译中,std::string、vector 这些类型定义来自系统头,它们参与模板实例化、SFINAE 判断,甚至影响 ODR。去掉它们,依赖图就只剩“皮相”,看不出哪些头真正拖慢编译。
正确做法是保留系统头,但过滤时做精准匹配:
- 先查系统头位置:
clang++ -E -dM /dev/null | grep __GLIBCXX__或看clang++ -v输出 - 再用
grep -v '/usr/include\|/Applications/Xcode.app/Contents/Developer/Toolchains.*\.h$'这类白名单式过滤,而不是粗暴用-MM - 若真要隔离业务逻辑,应该靠目录结构(如只处理
src/和include/下的路径),而不是靠编译器开关砍依赖
用 -MM 看似省事,实际让后续的增量编译、CI 缓存、依赖图分析全变成盲人摸象。
Makefile 里 include .d 文件前必须加 -include,且 .d 要早于规则生成
常见翻车是这样写 Makefile:
obj/%.o: src/%.cpp clang++ -c -o $@ $< -include deps/*.d
问题在于:deps/*.d 还没生成,-include 就去读,结果静默失败,Make 当作没这回事,后续头文件改了也不触发重编译。
正确顺序是:
- 先为每个
.cpp生成对应.d文件(用-M那套参数) - 再让
.o规则依赖它的.d,例如:obj/main.o: deps/main.d -
-include deps/*.d放在所有规则之后,且前面加短横线:-include deps/*.d(短横线表示“找不到也别报错”)
更稳妥的做法是用 $(shell find deps -name '*.d' 2>/dev/null) 动态展开,避免通配符在无文件时失效。依赖管理这事,差半步就全盘脱钩。


















