C++ Module 15 – Build systems, testing and tooling
clang-tidy, clang-format and static analysis
Lint C++ with clang-tidy, format it with clang-format in editors and CI, and compare what GCC's -fanalyzer and Clang's analyzer find in the same bugs.
What you will learn
- Run clang-tidy with a chosen set of checks on a CMake project
- Format code automatically with clang-format, in an editor and in CI
- Compare what GCC's and Clang's static analyzers find in the same code
- Plan a staged rollout of clang-tidy checks on an existing code base
Before you start
What you’ll learn · Before you start
What you will learn
- Run clang-tidy with a chosen set of checks on a CMake project
- Format code automatically with clang-format, in an editor and in CI
- Compare what GCC's and Clang's static analyzers find in the same code
- Plan a staged rollout of clang-tidy checks on an existing code base
Before you start
On this page
The order program of this lesson compiles without a single warning under -Wall -Wextra -Wpedantic, yet it prints
batch for with 0 items, because it reads two parameters after moving them away. Three kinds of tool read C++
source without running it. clang-format rewrites only the layout of the code, its indentation, spaces, line
breaks and the order of includes, from a style kept in a .clang-format file. clang-tidy is a
linter: A tool that reads source code and reports patterns that are likely bugs, older idioms or poor style, without running the program. clang-tidy is a linter for C and C++.: it compiles each file the way your build does, with the commands it reads from
compile_commands.json, and runs checks grouped by purpose, such as bugprone-* for likely bugs, modernize-* for
newer idioms and performance-* for needless copies.
Static analysis in the narrow sense follows the paths through a function, so it can see that a pointer is null on
one branch or that memory leaks on an early return: Clang’s analyzer (clang++ --analyze, also inside clang-tidy as
the clang-analyzer-* checks) and GCC’s -fanalyzer work this way. This lesson runs clang-tidy and clang-format
23.1.2 from Homebrew’s LLVM, Apple clang 21 and GCC 16.
Three tools, three jobs
A good setup formats every file, runs clang-tidy with a chosen set of checks, and uses an analyzer for the bugs that compiler warnings miss. Each tool looks at something different, and only one of them changes your code’s meaning:
| clang-format | clang-tidy | Static analyzers | |
|---|---|---|---|
| Looks at | Tokens and layout | The parsed program, file by file, with the build’s flags | The paths through each function, and the functions it calls |
| Changes the code | Yes, the layout only | Only with --fix, for checks that offer one |
Never |
| Needs | A .clang-format file |
compile_commands.json, and a .clang-tidy file for the choice of checks |
The compiler: clang++ --analyze, g++ -fanalyzer |
| Finds | Nothing; it formats | Bug patterns, old idioms, needless copies, naming and style | Leaks, null dereferences, use after free, double frees, uninitialised reads |
Static analysis takes longer than the other two jobs and its reports need more judgement, so it usually runs in CI rather than on every save. Formatting is the cheapest job, which is why editors run clang-format whenever a file is saved. The diagram shows what each of the three reads:
Three tools read the same files for three different jobs
Text description of the diagram
At the top, the project's .cpp and .hpp files have an arrow, labelled "the same files", to a column of three tools.
- clang-format reads the .clang-format file and rewrites the layout of the code.
- clang-tidy reads compile_commands.json and the .clang-tidy file, and reports findings, or fixes some of them with --fix.
- The static analyzers, clang++ --analyze and g++ -fanalyzer, follow the paths through each function and only report.
Below the column, a box for CMake with CMAKE_EXPORT_COMPILE_COMMANDS has an arrow to the tools: CMake writes the compile_commands.json file that clang-tidy reads.
Checkpoint
Check: Formatter, linter and analyzer
Checks, settings and analyzers to know by name
- Check groups such as
bugprone,modernize,performance,readabilityandcppcoreguidelinescollect checks with one purpose. - A
.clang-tidyfile chooses the checks, the warnings that become errors and the headers that are checked. NOLINTcomments silence one finding on one line, naming the check and, by convention, the reason.run-clang-tidyruns clang-tidy over every file ofcompile_commands.jsonin parallel.CMAKE_CXX_CLANG_TIDYmakes CMake run clang-tidy on each file while it builds.- GCC’s
-fanalyzerfollows paths through C code and, since GCC 16, through simple C++ code too. - Clang’s analyzer runs with
clang++ --analyze, or as clang-tidy’sclang-analyzer-*checks. git clang-formatformats only the lines that a change touches.
clang-tidy finds a use-after-move
This program moves its two parameters into a Batch and then prints from the parameters. It compiles without a
single warning under -Wall -Wextra -Wpedantic:
C++ · tidy/src/orders.cpp
// Collects order lines into a batch, then reports the batch. Compiles without a single warning.
#include <print>
#include <string>
#include <utility>
#include <vector>
struct Batch {
std::string customer;
std::vector<std::string> items;
};
Batch make_batch(std::string customer, std::vector<std::string> items) {
Batch batch{std::move(customer), std::move(items)};
std::println("batch for {} with {} items", customer, items.size());
return batch;
}
int main() {
const Batch b = make_batch("Asha", {"rice", "dal", "ghee"});
std::println("{} has {} items", b.customer, b.items.size());
} Text · tidy/CMakeLists.txt
cmake_minimum_required(VERSION 3.28...4.4)
project(tidy LANGUAGES CXX)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_CXX_SCAN_FOR_MODULES OFF)
# clang-tidy reads the compiler command of each file from build/compile_commands.json.
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
add_executable(orders src/orders.cpp)
add_executable(orders_fixed src/orders_fixed.cpp)
foreach(target orders orders_fixed)
target_compile_features(${target} PRIVATE cxx_std_23)
target_compile_options(${target} PRIVATE -Wall -Wextra -Wpedantic)
endforeach() from session import run
# Configure once to write compile_commands.json, then run one clang-tidy check on the program and on its fix
# (2>/dev/null leaves out clang-tidy's own counts on standard error, such as "1 warning generated.").
run("""
cd tidy
cmake -S . -B build -G Ninja
""", keep=r"^-- Build files")
run("""
cmake --build build
./build/orders
clang-tidy --version
clang-tidy -p build --checks='-*,bugprone-use-after-move' src/orders.cpp 2>/dev/null
""")
# CMake 4 leaves the SDK to Apple's compiler; Homebrew's clang-tidy needs it in the compile commands.
run("""
cmake -S . -B build -DCMAKE_OSX_SYSROOT=macosx
""", keep=r"^-- Build files")
run("""
grep -m1 -o 'isysroot [^ ]*' build/compile_commands.json
clang-tidy -p build --checks='-*,bugprone-use-after-move' src/orders.cpp 2>/dev/null
clang-tidy -p build --checks='-*,bugprone-use-after-move' src/orders_fixed.cpp 2>/dev/null
""") Output
$ cd tidy
$ cmake -S . -B build -G Ninja
-- Build files have been written to: build
[8 lines not shown]
$ cmake --build build
[1/4] Building CXX object CMakeFiles/orders.dir/src/orders.cpp.o
[2/4] Building CXX object CMakeFiles/orders_fixed.dir/src/orders_fixed.cpp.o
[3/4] Linking CXX executable orders
[4/4] Linking CXX executable orders_fixed
$ ./build/orders
batch for with 0 items
Asha has 3 items
$ clang-tidy --version
Homebrew LLVM version 23.1.2
Optimized build.
$ clang-tidy -p build --checks='-*,bugprone-use-after-move' src/orders.cpp 2>/dev/null
src/orders.cpp:2:10: error: 'print' file not found [clang-diagnostic-error]
2 | #include <print>
| ^~~~~~~
[exit status 1]
$ cmake -S . -B build -DCMAKE_OSX_SYSROOT=macosx
-- Build files have been written to: build
[2 lines not shown]
$ grep -m1 -o 'isysroot [^ ]*' build/compile_commands.json
isysroot /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk
$ clang-tidy -p build --checks='-*,bugprone-use-after-move' src/orders.cpp 2>/dev/null
src/orders.cpp:14:58: warning: 'items' used after it was moved [bugprone-use-after-move]
14 | std::println("batch for {} with {} items", customer, items.size());
| ^
src/orders.cpp:13:38: note: move occurred here
13 | Batch batch{std::move(customer), std::move(items)};
| ^
$ clang-tidy -p build --checks='-*,bugprone-use-after-move' src/orders_fixed.cpp 2>/dev/null
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 tidy_session.py
Step through make_batch with the values of that run, then the finding and the miss:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- Two parameters.
make_batch("Asha", {"rice", "dal", "ghee"})takes both parameters by value, socustomerholds Asha anditemsholds three strings. - The name moves. Line 13 builds
batchand movescustomerinto it.batch.customernow holds Asha, and the parameter is left moved-from: in this run, an empty string. - The items move. The same line moves
itemsintobatch.items, which now holds the three strings. The parameter is left empty too. - Read after the move. Line 14 prints from the parameters, not from
batch, so the program printedbatch for with 0 items: an empty name and a count of 0, with no warning from the compiler. - One finding. With the SDK found,
bugprone-use-after-movereported'items' used after it was moved, and a note pointed at the move on line 13. - One miss. It said nothing about
customer. The check counts passing a variable by non-const reference as giving it a new value, andstd::printlntakes its arguments by forwarding reference. - The fix. The fixed file reads from
batchinstead of the parameters, and clang-tidy is silent on it.
The program ran and printed batch for with 0 items: it read the empty, moved-from parameters. clang-tidy’s first
run never reached the check. It stopped at 'print' file not found, because CMake 4 leaves the choice of SDK to
Apple’s compiler, so the compilation database: The file compile_commands.json: the exact compiler command of every source file of a build. CMake writes it when CMAKE_EXPORT_COMPILE_COMMANDS is ON, and tools such as clang-tidy and editors read it. More in “CMake for C++: targets, properties and presets” names no SDK, and Homebrew’s
clang-tidy cannot find the standard library without one. Configuring with -DCMAKE_OSX_SYSROOT=macosx, the setting
CMake’s release notes give for forcing an SDK, put -isysroot into every command, and the second run reported
'items' used after it was moved, with a note showing where the move happened.
clang-tidy flagged items and not customer, although both were read after the move. Its documentation explains
why: passing a variable to a function by non-const reference counts as giving it a new value, since the function may
be filling an out-parameter, and std::println takes its arguments by forwarding reference. The fixed file reads
from batch instead, and clang-tidy is silent on it.
Checkpoint
Check: clang-tidy on a use-after-move
clang-format fixes the layout in one command
#include <vector>
#include <string>
#include <algorithm>
// Ranks players by score; written in a hurry.
struct Player{std::string name;int score;};
std::vector<Player> top_three(std::vector<Player> players){
std::ranges::sort(players,[](const Player&a,const Player&b){return a.score>b.score;});
if(players.size()>3) players.resize(3);
return players;} from session import run
# The style file, the check that CI runs (it changes nothing), the fix, and the check again.
run("""
cd format
cat .clang-format
clang-format --version
""")
run("""
clang-format --dry-run --Werror messy.cpp
""", keep=r"error:", max_lines=4)
run("""
clang-format --dry-run messy.cpp 2>&1 | grep -c clang-format-violations
clang-format -i messy.cpp
clang-format --dry-run --Werror messy.cpp
cat messy.cpp
xcrun clang-format --version
""") Output
$ cd format
$ cat .clang-format
# The project's style: LLVM's, with 4-space indents and 100 columns. clang-format finds this file next to the code.
BasedOnStyle: LLVM
IndentWidth: 4
ColumnLimit: 100
$ clang-format --version
Homebrew clang-format version 23.1.2
$ clang-format --dry-run --Werror messy.cpp
messy.cpp:1:1: error: code should be clang-formatted [-Wclang-format-violations]
messy.cpp:5:14: error: code should be clang-formatted [-Wclang-format-violations]
messy.cpp:5:15: error: code should be clang-formatted [-Wclang-format-violations]
messy.cpp:5:32: error: code should be clang-formatted [-Wclang-format-violations]
[62 lines not shown]
[exit status 1]
$ clang-format --dry-run messy.cpp 2>&1 | grep -c clang-format-violations
22
$ clang-format -i messy.cpp
$ clang-format --dry-run --Werror messy.cpp
$ cat messy.cpp
#include <algorithm>
#include <string>
#include <vector>
// Ranks players by score; written in a hurry.
struct Player {
std::string name;
int score;
};
std::vector<Player> top_three(std::vector<Player> players) {
std::ranges::sort(players, [](const Player &a, const Player &b) { return a.score > b.score; });
if (players.size() > 3)
players.resize(3);
return players;
}
$ xcrun clang-format --version
Apple clang-format version 21.0.0 (clang-2100.3.34.2)
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 format_session.py
The same session, one command at a time:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- The style.
.clang-formatasks for LLVM's style with 4-space indents and lines of up to 100 columns. clang-format finds the file next to the code it formats. - The CI check.
clang-format --dry-run --Werror messy.cppchanged nothing and ended with exit status 1. It counted 22 places, each reported ascode should be clang-formatted. - The fix.
clang-format -i messy.cpprewrote the file in place: the includes sorted, a space before&inconst Player &a, and the body of theifon a line of its own. - Checked again. The same check then printed nothing and passed, so a CI job that runs it on this file passes too.
- In the editor. Editors such as Visual Studio Code's C/C++ extension format with clang-format and read the same
.clang-format, so a file can be formatted each time it is saved.
--dry-run --Werror is the check a CI job runs: it changes nothing and fails when the file is not formatted. Here it
counted 22 places, then clang-format -i rewrote the file in place and the same check passed. The result shows the
LLVM style’s choices: includes sorted, a space before & in const Player &a, and the body of the if on its own
line. Apple’s Command Line Tools have their own clang-format, version 21, reached as xcrun clang-format; editors
such as Visual Studio Code’s C/C++ extension also format with clang-format and read the same .clang-format file.
Key idea
clang-format changes how the code looks and nothing else, so it can run on every save, and its --dry-run --Werror
check can stop any change that is not formatted.
Checkpoint
Check: clang-format in editors and CI
Check yourself
Ten questions on the formatter, the linter and the analyzers as this page has shown them. The weakest idea comes first, and each answer is explained the moment you choose it.
Practice that adapts to you
Practise this lesson
10 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Rolling out clang-tidy on a project, one group of checks at a time, with counts Pro: opens with a Pro, Premium or Ultimate pass
Turning on every check at once buries a team in findings it cannot fix in one go. Counting what each group reports on a small project shows which groups pay off first and which mostly express a taste.
In this part:
- Counting each group on one project
- Stage 1, and how to treat its findings
- Checks that need settings before they help
2 runnable examples (C++ (inventory.cpp), C++ (main.cpp), C++ header, Python) · about 4 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
What the analyzers find: GCC, Clang and the compiler on five bugs Pro: opens with a Pro, Premium or Ultimate pass
Compiler warnings, Clang’s analyzer and GCC’s analyzer were each given the same five bugs. The results differ more than their documentation suggests, and the reasons are worth knowing before you trust a clean report.
In this part:
- Five bugs, five tools
- Why Clang’s analyzer skipped the null pointer
- Why GCC’s analyzer found everything, then nothing
- Static and run-time tools see different bugs
2 runnable examples (C++, Python) · about 4 minutes
Opening this part with your pass…Pro part. It opens with the same pass as the part above.
Formatting in CI without churn: one pinned version, and options older versions reject Pro: opens with a Pro, Premium or Ultimate pass
A formatter only helps if everyone gets the same output. Two pinned versions of clang-format show how a team ends up reformatting the same lines back and forth, and how to stop it.
In this part:
- Same style name, different output
- Options that older versions reject
- A setup that stays quiet
- The one big reformatting commit
2 runnable examples (C++, Code, Python) · about 4 minutes
Opening this part with your pass…Pro part. It opens with the same pass as the part above.
Interview questions
-
Fresher · Warm-up
What is the difference between clang-format, clang-tidy and a static analyzer such as clang --analyze?
clang-format changes only the layout of code, its indentation, spacing, line breaks and include order, from the style in a .clang-format file, and never what the code means. clang-tidy is a linter: it parses each file with the flags from compile_commands.json and runs checks for bug patterns, needless copies, older idioms and style, some of which can fix the code with --fix. A static analyzer follows the paths through each function, so it can find a leak on an early return, a null pointer on one branch or a read after delete, without running the program. Clang's analyzer also runs inside clang-tidy as the clang-analyzer checks, and GCC has its own with -fanalyzer. Teams usually format on every save, run clang-tidy in CI, and use an analyzer for bugs that warnings miss.
-
Senior to staff
Your team turns on clang-tidy with every check and gets 2,300 findings on a 60,000-line code base. How do you roll it out without stopping work, and how do you decide which checks stay?
-
Mid to senior
The clang-format check in CI keeps failing on files that developers formatted locally, and each fix produces new differences elsewhere. What is going on, and how do you set it up so that it never happens again?
-
Mid to senior
A colleague says a module is safe because clang-tidy and the static analyzer report nothing on it. What can those tools miss, and what would you add before trusting the module?
Key takeaways
- clang-format changes layout only, clang-tidy runs checks on the program as the build compiles it, and analyzers follow paths through the code.
- clang-tidy needs
compile_commands.json; on macOS with CMake 4, configure with-DCMAKE_OSX_SYSROOT=macosxso it finds the SDK. - A check can miss what it was built for:
bugprone-use-after-movetreats passing by non-const reference as a new value. - Run
clang-format --dry-run --Werrorin CI, andclang-format -ior the editor’s formatter while you write.
Terms in this lesson
- linter
- A tool that reads source code and reports patterns that are likely bugs, older idioms or poor style, without running the program. clang-tidy is a linter for C and C++.
- static analysis
- Following the paths through a function without running the program, so that a tool can report a leak on an early return or a null pointer on one branch. clang++ --analyze and g++ -fanalyzer do it.
- use after move
- Reading a variable after std::move has handed its contents to another object. The moved-from object is still valid, but its value is not what it was: in this track's example, an empty string and an empty vector.
References
- Clang-Tidy (LLVM Project)
- bugprone-use-after-move (LLVM Project)
- ClangFormat (LLVM Project)
- Clang-Format Style Options (LLVM Project)
- Clang Static Analyzer (LLVM Project)
- Options That Control Static Analysis (GCC manual) (GNU Project)
- GCC 16 Release Series, Changes, New Features, and Fixes (GNU Project)
- llvm (Homebrew formula) (Homebrew)
- CMAKE_EXPORT_COMPILE_COMMANDS (Kitware)
- CMake 4.0 Release Notes (macOS SDK selection) (Kitware)
- <LANG>_CLANG_TIDY (target property) (Kitware)
- clang-format wheels on PyPI (Python Package Index)
- git-blame: --ignore-revs-file (Git project)
- Edit and navigate C++ code in Visual Studio Code: code formatting (Microsoft)
Report a problem with this lesson
Kept only in this browser. Your Learn progress