ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

用clang-format和clang-tidy打造C++代码规范化工具链

用clang-format和clang-tidy打造C++代码规范化工具链 1. 为什么C项目需要一套规范化工具链1.1 C的“自由”是风格混乱的根源我先说个背景。C这门语言在设计上给了你极大的自由度你可以把类定义放进头文件也可以只在头文件里放引用声明你可以让一个函数在屏幕上占五十行也可以把它压成一行甚至同一个表达式有人喜欢先判空再解引用有人喜欢反着来。这种自由在单个作者的项目里还没什么问题一旦变成多人协作就立刻变成分歧的源头。换一个视角看Go 语言有 gofmtRust 有 rustfmt它们在语言设计阶段就把格式答案写好了所以那些社区很少为“花括号放哪一行”吵架。C 一直没有官方统一风格也没有官方格式工具Google、LLVM、WebKit、GNU 各有一套风格指南写出来的代码放到一起简直就是字体混排的灾难。也正是因为这种生态现状clang-format 和 clang-tidy 这几年才在事实上成为了主流C代码规范化工具的默认选择。所以我说代码规范化工具解决的第一个问题不是代码质量而是团队协作的摩擦成本。写代码的人不用再花心思猜队友喜欢什么风格评审的人不用再花时间纠正缩进和换行机器替你把这些都做了。这个收益在你刚开始引入工具时可能体会不明显但只要经历过一次因为格式问题在 code review 里来回拉扯的场景就能明白它有多值钱。1.2 规范化工具到底管哪些事很多人以为格式化就是拿个脚本把空格换一下其实完整的规范化链条要管三层格式、静态检查、流程约束。格式层最简单也最基础包括缩进、换行、括号位置、指针星号靠哪边、include 排序、行宽上限。它的目标只有一个让所有源代码看起来像同一个人写的。静态检查层则往前走了一大步它需要真正读懂代码语义去发现那些格式工具根本看不出来的问题比如变量声明之后没有初始化、容器遍历时在循环里修改迭代器、移动过的对象又被继续使用、动态内存没有释放路径等等。这些错误靠 code review 往往很难全部抓到因为人眼会疲劳机器不会。第三层是流程约束。也就是把前面两层接进构建系统和 CI 流水线让格式检查和静态检查成为合并代码之前必须通过的关卡。这一步做不做直接决定了规范化工具能不能长期活下去。如果只是装个 IDE 插件偶尔格式化换个电脑、换个开发者很快又会乱回去。我自己见过太多项目规范文件躺在仓库里吃灰真正提交代码的时候根本没人跑工具。1.3 工具链选型格式与检查要分层我的建议是主链条用 clang-format 加 clang-tidy再用 CMake 和 CI 做集成。这套组合的好处是它们都来自 LLVM 项目版本同步、配置风格一致而且 Clang 的前端处理 C 非常成熟对模板、宏这些复杂特性的解析能力远超普通的正则扫描工具。为什么不单独用 cpplint 或者 cppcheckcpplint 是 Google 风格检查器它的定位偏重风格约定比如文件名、头文件卫士、include 顺序这些规则值得参考但它不做深度语义分析。cppcheck 能做的静态检查clang-tidy 基本都能做而且 clang-tidy 基于真实 AST还能拿到编译选项误报率明显更低。所以我的建议是把 cpplint 当作风格补充而不是主力。如果你愿意折腾include-what-you-use 也值得放进工具链。它专门检查头文件依赖能帮你清理掉“我根本没直接用这个头文件但编译就是过了”的问题。它本质上做的也是规范化只不过管的是 include 层面的健康度。不过它的编译方式和配置比 clang-tidy 复杂一些我更建议团队在 clang-format 和 clang-tidy 稳定跑通之后再考虑。2. 主力工具详解clang-format、clang-tidy、CMake集成2.1 先分清边界格式、检查与构建各自负责什么我先打一个比方。clang-format 像是排版工人只负责把标点、缩进、换行摆整齐它不读内容也不关心这句话是否有逻辑问题。clang-tidy 则是校对编辑它逐字读完代码去发现可能出错的句子和逻辑漏洞。CMake 和 CI 则是出版流程里的审批环节任何稿件没过审核就不能正式发布。在实际项目里这三个角色必须有明确分工否则很容易把职责搞乱。我见过有团队让 clang-tidy 去管命名规范结果十几条命名检查一开报错量直接爆表最后只能把整个工具关掉。正确的做法是格式类的东西交给 clang-format 和少量 readability 规则语义风险交给 analyzer 和 bugprone 规则命名规范这类需要团队讨论的再单独启用。工具层级核心能力典型输出clang-format文本格式缩进、换行、括号、行宽、include排序格式化后的代码 / diffclang-tidy语义静态检查未初始化变量、空指针、资源泄漏、移动语义误用等诊断报告 / 修复建议CMake自定义target构建集成把格式检查和静态检查挂到构建流程可复用的构建目标CI流水线流程约束合并前强制阻断不合格代码流水线失败/成功信号2.2 clang-format 核心配置解读一份能直接用的 .clang-format 大概是这样的# .clang-format BasedOnStyle: Google IndentWidth: 4 TabWidth: 4 UseTab: Never ColumnLimit: 100 ContinuationIndentWidth: 4 BreakBeforeBraces: Attach AllowShortFunctionsOnASingleLine: Empty AllowShortIfStatementsOnASingleLine: Never PointerAlignment: Left DerivePointerAlignment: false SortIncludes: true IncludeBlocks: Regroup我在每个关键字段下面写一点选择理由这样你拿去改的时候就知道动了会有什么影响。BasedOnStyle 选 Google是因为 Google 风格是当前最被广泛接受的基准覆盖的边界情况最全。IndentWidth 设成 4、UseTab 设成 Never这是绝大多数项目通过“谈判”最终收敛出来的结果缩进宽度低于 4 在嵌套深的代码里会分不清层级用 Tab 则在不同编辑器里宽度不一样。ColumnLimit 设 100 而不是 80是考虑到现代显示器下的并排阅读体验同时也不至于让代码被折成一片碎纸。BreakBeforeBraces 选 Attach也就是左大括号跟在语句末尾这是 C 社区最常见的写法当然也有团队喜欢 Allman这件事没有对错关键是全组保持一致。AllowShortFunctionsOnASingleLine 设为 Empty意思是空函数体可以写成一行其余普通短函数强制按标准展开这是为了 diff 可读性。AllowShortIfStatementsOnASingleLine 设为 Never因为单行 if 最容易藏 bug返回值、空语句都容易被忽略。SortIncludes 设为 true头文件自动排序能省掉大量合并冲突也方便你一眼看出某个头文件到底加在哪。还有一个细节是 DerivePointerAlignment我建议设 false如果开了 trueclang-format 会看代码里已有指针星号的位置再决定统一到哪一边不同文件表现不一致统一风格的目标就被破坏了。关于 ColumnLimit 有一个很重要的实操经验如果你的老项目已经有大量超长行我建议第一个版本把 ColumnLimit 放宽到 120让批量格式化先跑通下一轮再收紧到 100。不然一次改动就好几万个 diff代码评审根本看不完团队第一反应也是反对和抵触。2.3 clang-tidy 的检查项与配置思路.clang-tidy 的配置核心是 Checks 字段我常用这样一份配置起步# .clang-tidy Checks: clang-diagnostic-*, clang-analyzer-*, bugprone-*, performance-*, readability-*, -readability-magic-numbers, -readability-function-cognitive-complexity, -misc-no-recursion WarningsAsErrors: HeaderFilterRegex: .* FormatStyle: file CheckOptions: readability-identifier-naming.VariableCase: lower_case readability-identifier-naming.ClassCase: CamelCaseclang-diagnostic-* 和 clang-analyzer-* 我建议直接开满这两类是编译器警告和静态分析器误报率极低发现的问题基本都是真问题。bugprone-* 重点抓容易出错的编程模式比如 use-after-move、signal handler 里调用非 async-signal-safe 函数这些问题靠肉眼很难看全。performance-* 建议逐步开放因为有些性能建议在特定业务场景下并不成立比如“按值传参更合适”这类建议在需要保留对象语义的项目里就不尽然。readability-* 是最需要谨慎的一类。里面有些规则很主观像 magic-numbers会把代码里所有裸数字都报警一项目几千条cognitive-complexity 会把一个大函数直接判死刑。这两项我默认关掉等代码复杂度和风格跟上了再重新考虑。命名规则我建议分步开启先只规范类名和变量名别一上来把所有标识符都套上。还有一个容易被忽略的字段是 HeaderFilterRegex它的作用是把检查范围限制在某个目录下否则 boost、fmt 这类第三方头文件会产生大量和你无关的告警。对确实需要保留的代码可以用 // NOLINT 或者 // NOLINTBEGIN ... // NOLINTEND 抑制告警但抑制时最好写上理由。检查类别的推荐态度我用一张表总结检查类别典型代表推荐态度clang-analyzer-*core.NullDereference, core.LeakCommon必开bugprone-*bugprone-use-after-move, bugprone-undefined-memory-manipulation必开performance-*performance-unnecessary-value-param建议分阶段开readability-*readability-identifier-naming, readability-magic-numbers谨慎开启misc-*misc-no-recursion视项目而定2.4 把检查嵌进CMake构建工具配置好了如果不接进构建流程靠人肉记忆去跑很快就废了。我习惯在 CMakeLists.txt 里加两个自定义 targetset(CXX_SOURCES src/main.cpp src/logger.cpp src/network.cpp ) find_program(CLANG_FORMAT clang-format) find_program(RUN_CLANG_TIDY run-clang-tidy) add_custom_target(format-check COMMAND ${CLANG_FORMAT} --dry-run --Werror -stylefile ${CXX_SOURCES} COMMENT Checking code format ) add_custom_target(lint COMMAND ${RUN_CLANG_TIDY} -p ${CMAKE_BINARY_DIR} -header-filter^${CMAKE_SOURCE_DIR}/ ${CXX_SOURCES} COMMENT Running clang-tidy )这里有几个点要提前准备好。第一一定要让 CMake 生成 compile_commands.json也就是在配置阶段加 -DCMAKE_EXPORT_COMPILE_COMMANDSONclang-tidy 需要靠它拿到每个文件的编译参数没有这份文件它连文件都分析不了或者分析得很浅。第二跑 lint 的速度并不快几千个文件的仓库全量跑一次可能要十几分钟所以我不建议把它绑在每次常规构建里而是做成 format-check 和 lint 这样的独立 target或者放到 CI 的单独步骤。第三find_program 要搭配版本检查我在第 4.3 节会展开讲版本不一致的问题。如果你想把流程做得更自动化可以写一个脚本统一处理比如先跑 clang-format 把格式规整再用 run-clang-tidy 把可自动修复的问题批量修掉。但自动修复后的代码必须再走一遍人工评审因为 clang-tidy 的某些自动修复会改变代码语义比如它可能会把建议的初始化方式应用到一个本意是需要延迟构造的变量上。3. 实战从一份旧代码到干净代码的完整过程3.1 第一步先定规范再动代码很多团队一上来就下载 clang-format对着全仓跑一遍 -i结果代码是统一了但没人知道按的什么规则统一的下次维护也没依据。正确做法是先把规范文件定下来让全组 review。我建议直接以某个成熟风格作为基准比如 Google 或 LLVM再根据团队偏好微调。为什么要选现成基准而不是从零开始配置因为成熟风格经受住了大量项目的检验里面很多边界情况比如宏定义、模板实参、lambda 表达式的换行规则都已经覆盖过了从零配置会遗漏很多细节过一段时间又要回头补规则。规范文件定好后提交到仓库根目录。然后建一个文件清单确保后续所有检查都覆盖到。这里尽量不要用通配符组织的临时清单第三方代码不参与规范检查。我通常会在项目里维护一个 CMake 变量或者写一个脚本里的路径白名单把 src 和 include 下的文件列进去第三方目录直接排除。这一步虽然看起来繁琐但它是后面所有自动化的基础。3.2 第二步批量格式化全仓格式化最简单但也是最容易翻车的一步。我习惯的做法是先用脚本输出变更统计再决定要不要分批。这里给一套我常用的脚本逻辑#!/bin/bash # format.sh FILES$(find src include -name *.cpp -o -name *.hpp -o -name *.h) for f in $FILES; do if echo $f | grep -q third_party/; then continue fi clang-format -i -stylefile $f || echo format failed: $f done注意几个点。find 的括号和 -o 优先级很坑如果项目已经用 git 管理直接用 git ls-files 提取仓库内文件会更稳然后用 grep -E 过滤目录。文件多的时候可以用 xargs -P 并行但 clang-format 本身很快瓶颈一般不在 CPU而在你要 review 的 diff 量。真正要重视的是提交策略。我强烈建议把“全仓格式化”和“功能改动”分开提交。格式化那天单独提交一个 commitcommit message 里写清楚。然后在仓库里放一个 .git-blame-ignore-revs 文件把这次 format commit 的哈希写进去并在 git config 或 CI 里启用 blame.ignoreRevsFile。否则以后 git blame 看到的每一行都是格式化当天的提交记录真正找 bug 就成考古了。如果你用 VS Code 配好了 C 环境安装一个 Clang-Format 扩展平时编辑保存时顺手格式化很舒服。但要控制住编辑器自动格式化只适合增量修改不适合用来做全仓统一因为不同 IDE 版本、不同系统可能跑出不同结果。全仓操作还是要走命令行脚本保证所有人用同一条路径。3.3 第三步静态检查并修掉真实问题格式统一之后静态检查才是真正能救代码命的环节。我第一轮跑 clang-tidy 时最大的感觉是“原来这些坑已经有人替我们踩过了”。跑法有两类全量和增量。全量适用于第一次治理直接 run-clang-tidy -p build src | tee tidy.log把报告导出。增量适用于日常开发只检查当前提交涉及的变更文件速度很快。增量检查需要拿到变更文件列表可以用 git diff 自动生成脚本比如 git diff --name-only origin/main...HEAD -- .cpp .h。拿到报告后先分类。真正的 bug 立刻修比如 use-after-move、assert(false) 之后还会继续执行、数组越界的分析结果。建议类的问题比如不必要的值拷贝按团队的习惯决定修不修。误报也一定有别急着硬改代码先用 NOLINT 注释把位置标记出来理由写清楚下一次再遇到类似模式就知道如何处理。注意 clang-tidy 的自动修复开关是 --fix 和 --fix-errors前者只修普通问题后者连有编译错误文件里的错误也会试着修后者风险很大我基本不用。并且修完自动修复项后要重新编译一次因为 clang-tidy 给出的某些修复会引入新的编译问题。3.4 第四步用 CI 把规范变成“硬约束”走到这一步规范化才真正闭环。没有 CI 卡点工具装得再全也会慢慢失效有了 CI 卡点哪怕新人第一次提交代码也能立刻知道自己的格式和检查没过关。这里给一个 GitHub Actions 的示例name: code-quality on: [pull_request] jobs: format-and-lint: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - name: Install clang-format and clang-tidy run: | sudo apt-get update sudo apt-get install -y clang-format-14 clang-tidy-14 - name: Configure with compile commands run: | cmake -B build -DCMAKE_EXPORT_COMPILE_COMMANDSON cmake --build build --target format-check - name: Run lint run: cmake --build build --target lint这个配置里我没写太多花活核心就两步配置编译、跑两个检查 target。里面有一个细节尤其重要安装的 clang-format 和 clang-tidy 版本要和本地开发环境保持一致。这里用 14 只是为了示例实际项目就用你们统一约定的版本。另外 CMake 配置时必须开 EXPORT_COMPILE_COMMANDS否则 clang-tidy 不知道每个源文件该用什么参数编译会直接略过很多文件。如果你想分两层反馈可以把 format-check 放进本地 pre-commit 钩子把 lint 放进 CI。因为格式问题本地就能秒级感知而静态检查耗时较长放在 CI 更合适。这样开发者提交前至少能自己先过一遍格式避免在 CI 排队等几分钟才发现格式没对。4. 常见问题与排查技巧实录4.1 格式化引发大范围diff如何保住历史第一个遇到的问题是全仓格式化完之后git diff --stat 显示改了一万多行。如果你在这个 commit 之后再提需求单code review 根本没法做每一行都改过了。我的处理办法是三步第一把格式化和功能改动放在不同 commit格式化 commit 独立存在。第二在仓库根目录写一个 .git-blame-ignore-revs 文件把这次提交的完整哈希挂上然后执行 git config blame.ignoreRevsFile .git-blame-ignore-revs。第三要求 CI 或者团队成员的 git 配置都启用这个文件这样以后 git blame 会自动跳过格式化那次改动。我踩过的一个坑是有些团队没有在拉分支前统一版本结果两个人各跑了一次 clang-format生成的结果不一样又产生一轮新的全仓 diff。这印证了版本统一问题的重要性。如果已经出现这种情况别急着重新格式化先把工具版本对齐到同一个 LLVM 版本再统一格式化一次。4.2 clang-tidy 误报怎么判断该信还是该忽略误报是 clang-tidy 绕不开的问题。最常见的是宏展开场景。比如你写了一个宏在宏展开时 clang-tidy 对某一处变量做了假设但实际业务中宏的展开结果因上下文不同而不同它给出的告警就可能是错的。处理误报我的顺序是先看告警对应的代码行确认是不是业务确实写错了再确认是否受宏或第三方头文件影响最后才是决定怎么屏蔽。单纯因为不想看到红色告警而屏蔽是懒不是理智。屏蔽时优先用 NOLINTBEGIN 和 NOLINTEND 包住一小段代码而不是在整个文件顶部加 // NOLINT否则会掩盖真正的新问题。屏蔽的位置最好由 code review 把一眼毕竟误报率高的告警类别本身也值得怀疑。还有一个经验不要把 clang-tidy 的所有检查都设成 WarningsAsErrors。我见过有团队把 readability-magic-numbers 设为 error结果任何数字都要定义常量改动量巨大最后大家干脆放弃工具。宁可在 CI 里把 analyzer 类设为阻断把风格建议类设置为提醒给团队留出缓冲空间。4.3 工具版本不一致导致格式漂移clang-format 在各个版本之间输出不是完全一致的尤其是 10、11、12 这几个版本对 lambda 换行和指针对齐的处理变化不少。我遇到过两个同事用同一份 .clang-format一个在 mac 上装 clang-format 15一个在 Windows 上装 clang-format 12两个人 formatter 跑出来的 diff 互相冲突陷入无限循环。治本的办法是统一版本。有几种常见方案一在 CI 容器里固定一个版本本地开发工具不做硬性要求只在提交时用 CI 验证。二项目内提供一个 setup 脚本或 Docker 镜像统一工具版本。三在 CMake 里加一个版本检查 target启动时提示当前工具版本。我实际项目里用的是第一个方案配合在 README 里写明推荐版本已经稳定跑了一年多。这里给一个简单的版本检查命令clang-format --version clang-tidy --version。把它写进 CI 第一步如果版本号不匹配直接失败并输出提示能避免很多莫名其妙的格式漂移问题。4.4 团队成员觉得“太麻烦”怎么办工具推行最大的坑往往不是技术而是人。我见过最典型的场景老大拍板引入 clang-tidy结果第一周报告出来几百条问题大家一看就头大第二周就没人跑了。我的落地经验是分阶段推进。第一周只跑 clang-format让所有人感受到“格式化之后代码变清爽了”第二周开 bugprone 和 analyzer把真正的 bug 摊在所有人面前这种价值最容易服人第三周开始放 performance 类至于 readability 的命名规则放到所有人适应了工具之后再讨论。每个阶段都要留出缓冲时间而不是一天内把所有规则全部打开。另外规范工具的定位应该是“帮人少犯错”不是“找人毛病”。在团队沟通时少说“你这里格式化不对”多说“我这边加了一条规则以后这块逻辑会自动提醒”。机器能判断的都交给机器人工 review 就只聊逻辑、设计和边界这才是规范化真正改变协作方式的地方。5. 避坑心得与后续扩展5.1 几条一定要记住的心得版本统一是地基。任何一个版本不一致都会让所有 CI 卡点变成薛定谔的通过。先解决格式再开静态检查不要试图一步到位。自动化检查应该成为团队默认流程而不是某几个人手动跑了给大家看看。所有告警抑制都必须被 review否则工具会变成精致的摆设。我自己体会最深的一点是引入规范化工具链不是为了让代码“好看”而是为了把人从重复劳动里解放出来。每次 CI 能自动拦住一个低级错误团队就能多花十分钟去讨论真正的设计问题。代码风格这件事机器做永远比人吵架高效。5.2 后续可以怎么扩展如果你已经把 clang-format 和 clang-tidy 跑顺下一步我建议看看 include-what-you-use。它专门做头文件依赖治理能让编译速度和分析效率都跟着提升。再下一步是可以结合 clangd 作为 IDE 的代码诊断后端这样开发者在编辑器里看到的问题和 CI 里一致反馈闭环会非常自然。代码规范化工具是一条持续演进的路。到了 C20/C23 的时代新特性越来越多静态检查规则也在不断增加。工具本身不是目的目的永远是让团队写代码时更自由、让代码被阅读时更轻松、让项目持续活得更久。我的建议是现在就选一条成熟的路线把它落地其余的都交给时间和迭代。
返回列表