Using Clang-Tidy
Integrating Clang-Tidy to enforce good practices and find bugs before we even compile
Static analysis is the process of analyzing code for potential errors without executing it. Static analysis tools simply read our code files to see if they can find any potential issues.
A linter is a type of static analysis tool that checks for stylistic errors, common programming mistakes, and violations of best practices.
Clang-Tidy is a highly configurable linter for C++, C, and Objective-C. It can detect everything from simple formatting issues to complex bugs like potential memory leaks or incorrect API usage.
Integrating Clang-Tidy
Clang-Tidy is part of the LLVM Project. It's likely you already have it installed in your environment, particularly if you followed along with the earlier section on using .
You can verify Clang-Tidy is available in your environment in the usual way:
clang-tidy --version
LLVM (http://llvm.org/):
LLVM version 20.1.8
Optimized build.
From our build scripts, we can find clang-tidy
in the usual way, via find_program()
:
Files
cmake --preset default
...
-- clang-tidy found: D:/msys64/clang64/bin/clang-tidy.exe
...
We can now use this program using the same techniques we covered previously, by invoking it within a or encapsulating it within a .
However, CMake is aware of ClangTidy by default, and we can use it within our build simply by setting the CMAKE_CXX_CLANG_TIDY
variable. This tells CMake to run clang-tidy
alongside the compiler for every source file:
cmake/Tidy.cmake
cmake_minimum_required(VERSION 3.23)
find_program(CLANG_TIDY_EXE "clang-tidy")
if(CLANG_TIDY_EXE)
message(STATUS "clang-tidy found: ${CLANG_TIDY_EXE}")
set(CMAKE_CXX_CLANG_TIDY ${CLANG_TIDY_EXE})
else()
message(WARNING "clang-tidy not found!")
endif()
If we configure and build now, then we may already see some clang-tidy warnings, depending on the state of our code base:
cmake --preset default
cmake --build --preset default
warning: Value stored to '_' during its initialization is never read [clang-analyzer-deadcode.DeadStores]
10 | for (auto _ : state) {
| ^ ~
Suppressing Warnings
In large projects, linters will often output warnings or errors for sections of code that we're happy with, or don't want to refactor. To handle this, we can suppress the linter by adding specifically formated comments at or near the offending lines.
Adding a NOLINT
comment will ask the linter to ignore that line:
benchmarks/bench_main.cpp
#include <benchmark/benchmark.h>
#include <greeter/Greeter.h>
#include <string>
static void BM_Greeter_Greet(benchmark::State& state) {
std::string name(state.range(0), 'x');
Greeter g(name);
for (auto _ : state) {// NOLINT
std::string result = g.greet();
benchmark::DoNotOptimize(result);
}
}
BENCHMARK(BM_Greeter_Greet)->Range(8, 32768);
BENCHMARK_MAIN();
We can also ask the linter to ignore the next line using NOLINTNEXTLINE
:
// NOLINTNEXTLINE
for (auto _ : state) {
std::string result = g.greet();
benchmark::DoNotOptimize(result);
}
We can ignore an entire block by wrapping it between NOLINTBEGIN
and NOLINTEND
comments:
// NOLINTBEGIN
for (auto _ : state) {
std::string result = g.greet();
benchmark::DoNotOptimize(result);
}
// NOLINTEND
All of these NOLINT
variations allow us to specify the rule we want to suppress within brackets. The name of the rull that was violated will be included in the warning or the error that was emitted:
// NOLINTNEXTLINE(clang-analyzer-deadcode.DeadStores)
for (auto _ : state) {
std::string result = g.greet();
benchmark::DoNotOptimize(result);
}
This is generally recommended, as it clarifies to readers what the comment is actually suppressing.
Additionally, if a new violation is added to the area, that new violation will emit a warning to attract our attention, rather than also get suppressed.
We can suppress multiple rules by comma-seperating them:
// NOLINTNEXTLINE(rule-1,rule-2)
Configuring Clang-Tidy
Clang-Tidy is highly configurable. The primary way of doing this is through a .clang-tidy
file - note the initial .
in the file name. This file is stored in your project root and shared among the team.
The most notable thing we configure is which rules we want Clang-Tidy to check for. Clang-Tidy can scan for around 300 issue at the time of writing. You can find them all on the official documentation.
Let's enable a check that suggests using nullptr
instead of the old NULL
macro:
.clang-tidy
Checks: '-*,modernize-use-nullptr'
The -*
syntax tells Clang-Tidy to disable all checks. The following modernize-use-nullptr
then tells it to renable checking for just that rule.
We can add further rules using comma-separation. The following configuration also discourages the use of std::endl
:
Checks: '-*,modernize-use-nullptr,performance-avoid-endl'
Let's introduce some code that violates the modernize-use-nullptr
. Historically, the NULL
keyword was often used to represent null pointers. NULL
is a macro typically defined as the integer 0
.
Setting a pointer to 0
technically works as a way to represent a null pointer, but it causes issues in overload resolution:
void f(int);
void f(char*);
f(NULL); // calls f(int), not f(char*)
C++11 introduced the explicit nullptr
keyword, which is now the best practice:
void f(int);
void f(char*);
f(nullptr); // calls f(char*)
We'll violate this rule in Greeter.cpp
so we can confirm that our configuration is working:
greeter/src/Greeter.cpp
#include <greeter/Greeter.h>
Greeter::Greeter(std::string name)
: name_(std::move(name)) {}
std::string Greeter::greet() const {
int* SomePtr{NULL};
return "Hello, " + name_ + "!";
}
Let's rebuild our project:
cmake --build --preset default
During compilation, Clang-Tidy should run and emit a warning directly in our build log, pointing out the violation and often suggesting a fix:
.../greeter/src/Greeter.cpp:20:13: warning: use nullptr [modernize-use-nullptr]
if (ptr == NULL) {
^
nullptr
Emitting Errors
Our previous rule emits a warning when violations are detected. If we care a lot about ensuring a specific rule is followed, we can upgrade that to an error instead. We can do this using the WarningAsErrors option in our .clang-tidy
:
.clang-tidy
Checks: '-*,modernize-use-nullptr'
# Treat specific warnings as errors:
WarningsAsErrors: 'modernize-use-nullptr'
# Alternatively, treat all warnings as errors:
WarningsAsErrors: '*'
If we rebuild the project, our use of NULL
over nullptr
should now generate an error.
Our build system may skip rebuilding our targets if the only thing that has changed is a .clang-tidy
file. We can delete our outputs and force them to be recompiled by passing the --clean-first
argument:
cmake --build --preset default --clean-first
D:/Projects/cmake/greeter/src/Greeter.cpp:7:16: error: use nullptr [modernize-use-nullptr,-warnings-as-errors]
7 | int* SomePtr{NULL};
| ^~~~
| nullptr
1 warning treated as error
Configuring Clang-Tidy using Variables
We can configure Clang-Tidy by extending our use of the CMAKE_CXX_CLANG_TIDY
variable. This approach would override any rules we had set in our .clang-tidy
file, if we had one.
For example, to override the warnings-as-errors configuration we set in our .clang-tidy
, we could set that argument to -*
, removing the errors for all rules:
cmake/Tidy.cmake
cmake_minimum_required(VERSION 3.23)
find_program(CLANG_TIDY_EXE "clang-tidy")
if(CLANG_TIDY_EXE)
message(STATUS "clang-tidy found: ${CLANG_TIDY_EXE}")
set(CMAKE_CXX_CLANG_TIDY ${CLANG_TIDY_EXE}
--warnings-as-errors=-*
)
else()
message(WARNING "clang-tidy not found!")
endif()
As with any variable, this could also be set from a preset:
CMakePresets.json
{
"version": 3,
"configurePresets": [
// ... other presets
{
"name": "no-lint-errors",
"inherits": "default",
"cacheVariables": {
"CMAKE_CXX_CLANG_TIDY":
"clang-tidy;--warnings-as-errors=-*"
}
}
]
// ...
}
Practical Example: Performance Checks
Finally, let's cover a slightly more advanced configuration. It's a common mistake in C++ to pass a large object, like a std::string
, to a function by value when it could be passed by const
reference. Passing by value creates an unnecessary and potentially expensive copy.
First, let's update our .clang-tidy
with some further checks. Our previous configuration was checking for violations of the modernize-use-nullptr
rule. The preference for using nullptr
relates to code modernization, so it is prefixed with the modernize-
tag. All other rules are namespaced in similar ways. For example:
- Checks that relate to performance have a name starting with
performance-
- Checks relating to general code reability start with
readability-
- Checks that scan for syntax that is a common source of bugs are prefixed with
bugprone-
We can use this to configure large categories of checks at once. The following config file enables all checks relating to modernization and performance. It also specifically upgrades violations of the unnecessary passing-by-value rule to be errors:
.clang-tidy
Checks: '-*,modernize-use-nullptr,performance-*'
WarningsAsErrors: 'performance-unnecessary-value-param'
Let's update Greeter.cpp
to make this mistake:
greeter/src/Greeter.cpp
#include <greeter/Greeter.h>
Greeter::Greeter(std::string name)
: name_(std::move(name)) {}
namespace {
bool should_ignore(std::string name) {
return name == "Bob";
}
}
std::string Greeter::greet() const {
if (should_ignore(name_)) return "";
return "Hello, " + name_ + "!";
}
Now, when we build, Clang-Tidy will analyze Greeter.cpp
. Because we set WarningsAsErrors
for the rule we just violated, the check will fail the build and provide a detailed message explaining the problem and suggesting the fix.
Greeter.cpp:7:32: warning: the parameter 'name' is copied for each invocation but only used as a const reference; consider making it a const reference [performance-unnecessary-value-param]
7 | bool should_ignore(std::string name) {
| ^
| const &
Remember, we now also have benchmarks to quantify this problem. Previously, the BM_Greeter_Greet/32768
benchmark was reporting around 1,500 ns on my machine.
With our latest changes, the run time of our greet()
function has increased by around 60%:
cmake --build --preset release
./build/benchmarks/GreeterBenchmarks
------------------------------------------------------
Benchmark Time CPU Iterations
------------------------------------------------------
...
BM_Greeter_Greet/32768 2440 ns 2382 ns 295153
If we do what Clang-Tidy suggests and update it to use a const
reference, our new feature only adds around 15%:
greeter/src/Greeter.cpp
#include <greeter/Greeter.h>
Greeter::Greeter(std::string name)
: name_(std::move(name)) {}
namespace {
bool should_ignore(const std::string& name) {
return name == "Bob";
}
}
std::string Greeter::greet() const {
if (should_ignore(name_)) return "";
return "Hello, " + name_ + "!";
}
cmake --build --preset release
./build/benchmarks/GreeterBenchmarks
-------------------------------------------------------
Benchmark Time CPU Iterations
-------------------------------------------------------
...
BM_Greeter_Greet/32768 1722 ns 1713 ns 401408
Clang-Tidy has hundreds of checks covering performance, readability, modern C++ idioms, and more. You can find a full list and detailed explanations on the official Clang-Tidy documentation page.
Other Analysis Tools
The tools we've covered are just the beginning. The C++ ecosystem has a huge set of analysis tools, and most can be integrated into CMake using the patterns we've learned. Here are a few more to be aware of:
Dependency Analysis
Over time, #include
directives in C++ files tend to become a mess. You might include headers you don't need, or forget to include headers you do need (relying on them being included transitively).
include-what-you-use
(IWYU) is a tool that analyzes your code and tells you exactly which headers you should add or remove, helping to keep your codebase clean and improve compilation times.
Security Analysis
Many static analysis tools, including commercial ones like SonarQube and advanced configurations of Clang-Tidy, can be used to scan for common security vulnerabilities.
Summary
In this lesson, we explored static analysis as the final pillar of our automated code quality toolkit.
- Static Analysis: Tools that analyze code for bugs and style issues without executing it.
- Clang-Tidy: A powerful, configurable linter for C++. We integrated it into our build by setting the
CMAKE_CXX_CLANG_TIDY
variable. - Configuration as Code: The behavior of Clang-Tidy is controlled by a
.clang-tidy
file in the project root, allowing you to define and share a consistent set of checks for the whole team. - Enforcing Quality: We saw how Clang-Tidy can automatically find performance pitfalls and suggest modern C++ idioms like
nullptr
. - Advanced Clang-Tidy Capabilities: This is only the tip of the iceberg of what Clang-Tidy can help with. The official documentation page has much more.