Cppcheck — Static Analysis
Cppcheck for C++ — running checks, suppressing false positives, CMake integration, CI setup, and comparison with clang-tidy.
TL;DR
Cppcheck is a static analysis tool that finds bugs (null pointer dereference, out-of-bounds, memory leaks, undefined behavior) without compiling. Complements compiler warnings and clang-tidy — runs on code that doesn't compile cleanly.
Installation
# Ubuntu/Debian
sudo apt install cppcheck
# macOS
brew install cppcheck
# Windows (Chocolatey)
choco install cppcheck
# Build from source
git clone https://github.com/danmar/cppcheck.git
cmake -B build && cmake --build buildBasic Usage
# Check a single file
cppcheck main.cpp
# Check a directory recursively
cppcheck src/
# Show all messages (not just errors)
cppcheck --enable=all src/
# Specific checks
cppcheck --enable=warning,performance,portability src/
# C++17 standard
cppcheck --std=c++17 src/
# Define macros / includes
cppcheck -DDEBUG -I include/ src/Check Categories
error - definite bugs (null pointer, OOB, use-after-free)
warning - possibly buggy code (uninitialized variables, etc.)
style - code style issues (unused variables, redundant code)
performance - performance suggestions (unnecessary copies)
portability - non-portable constructs
information - informational messages
all - enable everything above
unusedFunction- functions never called (whole-project scan)Output Format
# Default output
cppcheck src/
# [src/main.cpp:42]: (error) Null pointer dereference: ptr
# XML output for CI integration
cppcheck --xml --xml-version=2 src/ 2> report.xml
# Template format
cppcheck --template="{file}:{line}: {severity}: {message} [{id}]" src/Suppressing False Positives
// Suppress inline (preferred for one-off)
// cppcheck-suppress nullPointer
ptr->method();
// Suppress with reason
// cppcheck-suppress uninitvar ; reason: initialized in init() before use
int x;
// Suppress by ID in suppression file
// cppcheck-suppress arrayIndexOutOfBounds
arr[i] = val;# Suppression file (suppressions.txt)
# nullPointer:src/legacy.cpp:42
# uninitvar
cppcheck --suppressions-list=suppressions.txt src/CMake Integration
find_program(CPPCHECK cppcheck)
if(CPPCHECK)
set(CMAKE_CXX_CPPCHECK
${CPPCHECK}
--std=c++17
--enable=warning,performance,portability
--suppress=missingIncludeSystem
--error-exitcode=1
--inline-suppr
)
endif()CI Example (GitHub Actions)
- name: Run cppcheck
run: |
cppcheck \
--std=c++17 \
--enable=all \
--suppress=missingIncludeSystem \
--error-exitcode=1 \
--xml --xml-version=2 \
src/ 2> cppcheck-report.xml
- name: Upload cppcheck report
if: failure()
uses: actions/upload-artifact@v3
with:
name: cppcheck-report
path: cppcheck-report.xmlCppcheck vs clang-tidy
| cppcheck | clang-tidy | |
|---|---|---|
| Requires compilation | No | Yes (compile_commands.json) |
| Bug finding | Strong | Strong |
| Code modernization | No | Yes (modernize-* checks) |
| Performance checks | Basic | Strong |
| False positive rate | Low | Medium |
| Speed | Fast | Slower |
| Use together? | Yes — complementary | Yes |
Use both: cppcheck for quick scans and CI gates, clang-tidy for modernization and deeper analysis.
Using compile_commands.json for Better Analysis
Feeding cppcheck your build database eliminates the biggest source of false positives: missing include paths and undefined macros. Without it, cppcheck guesses at which headers exist and which preprocessor symbols are defined, leading to spurious warnings about identifiers it can't resolve.
# Generate compile_commands.json with CMake
cmake -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
# Point cppcheck at the database — it picks up exact include paths and defines
cppcheck --project=build/compile_commands.json
# Combine with check categories and error gating
cppcheck --project=build/compile_commands.json \
--enable=warning,performance \
--suppress=missingIncludeSystem \
--error-exitcode=1The --project flag is qualitatively different from -I flags: it reads the exact compiler invocation for each translation unit, including all -D defines, -I include paths, and -std flags, so cppcheck sees what the compiler sees. Manual -I flags are a fallback for projects without a build system that produces compile_commands.json.
# Manual include approach (less accurate — use --project when possible)
cppcheck -I include/ -I third_party/include/ -DNDEBUG --std=c++17 src/
# --project also accepts Visual Studio project files (.vcxproj / .sln)
cppcheck --project=MyApp.vcxprojSymlink the database to the project root so editors and other tools find it automatically:
ln -sf build/compile_commands.json compile_commands.jsonCommon Findings and What They Mean
Each cppcheck diagnostic has an ID that tells you exactly which check fired. The eight patterns below account for the majority of findings in real codebases.
nullPointerDereference
// BAD — cppcheck reports:
// [src/main.cpp:5]: (error) Null pointer dereference: p
void process(int* p) {
if (p == nullptr) {
log("null input");
}
*p = 42; // reached even when p is null
}
// FIX — return early on null
void process(int* p) {
if (p == nullptr) {
log("null input");
return;
}
*p = 42;
}uninitvar — uninitialized variable in branch
// BAD
// [src/parser.cpp:8]: (error) Uninitialized variable: result
int parse(const std::string& s) {
int result;
if (!s.empty()) {
result = std::stoi(s);
}
return result; // result is uninitialized when s is empty
}
// FIX
int parse(const std::string& s) {
int result = 0; // initialize to a safe default
if (!s.empty()) {
result = std::stoi(s);
}
return result;
}outOfBounds — array index past end
// BAD
// [src/buf.cpp:4]: (error) Array 'buf[8]' accessed at index 8, which is out of bounds
char buf[8];
for (int i = 0; i <= 8; ++i) { // <= should be <
buf[i] = 0;
}
// FIX
for (int i = 0; i < 8; ++i) {
buf[i] = 0;
}memleak — malloc without free in error path
// BAD
// [src/alloc.cpp:9]: (error) Memory leak: data
void load(const char* path) {
char* data = (char*)malloc(4096);
FILE* f = fopen(path, "r");
if (!f) {
return; // data is never freed
}
fread(data, 1, 4096, f);
fclose(f);
free(data);
}
// FIX — free on every exit path
void load(const char* path) {
char* data = (char*)malloc(4096);
FILE* f = fopen(path, "r");
if (!f) {
free(data);
return;
}
fread(data, 1, 4096, f);
fclose(f);
free(data);
}resourceLeak — file handle not closed
// BAD
// [src/io.cpp:6]: (error) Resource leak: f
bool hasContent(const char* path) {
FILE* f = fopen(path, "r");
if (!f) return false;
char c;
if (fread(&c, 1, 1, f) != 1) {
return false; // f is never closed on this path
}
fclose(f);
return true;
}
// FIX — prefer RAII; or close on every branchuseStlAlgorithm — prefer std::for_each
// WARNING — cppcheck suggests std::for_each (or range-for)
// [src/proc.cpp:4]: (style) Consider using std::for_each instead of a raw loop
for (size_t i = 0; i < items.size(); ++i) {
process(items[i]);
}
// FIX
std::for_each(items.begin(), items.end(), process);
// or simply:
for (const auto& item : items) { process(item); }constParameter — parameter should be const
// WARNING
// [src/util.cpp:1]: (style) Parameter 'name' can be declared as const reference
void print(std::string name) { // passed by value and never modified
std::cout << name << '\n';
}
// FIX
void print(const std::string& name) {
std::cout << name << '\n';
}knownConditionTrueFalse — logic bug
// WARNING
// [src/check.cpp:5]: (style) Condition 'x > 0 && x < 0' is always false
int x = getValue();
if (x > 0 && x < 0) { // impossible: no integer satisfies both
handle();
}
// FIX — likely a copy-paste error; the second condition should be a different variableCppcheck MISRA C++ Compliance
Cppcheck ships a Python addon system that layers rule-set checking on top of its core analysis. Addons are .py scripts in /usr/share/cppcheck/addons/ (or the source tree's addons/ directory) that post-process the cppcheck dump format.
# MISRA C++ 2023 checking
# misra.txt must be obtained separately under MISRA license
cppcheck --dump src/ # generate src/*.dump files
python3 /usr/share/cppcheck/addons/misra.py \
--rule-texts=misra.txt src/*.dump
# Or use --addon to run in a single step
cppcheck --addon=misra.py --rule-texts=misra.txt src/The misra.txt rule-text file contains the human-readable MISRA rule descriptions. It must be licensed from the MISRA consortium separately — cppcheck only ships the checker, not the rule text.
# CERT C++ Secure Coding rules (no separate license needed)
cppcheck --addon=cert.py src/
# Thread safety analysis (data races, lock ordering)
cppcheck --addon=threadsafety.py src/
# Y2038 / time_t overflow checks
cppcheck --addon=y2038.py src/
# Combine multiple addons
cppcheck --addon=cert.py --addon=threadsafety.py \
--project=build/compile_commands.jsonAddons run after the main analysis pass and access the same AST dump, so they add no compilation overhead. Each addon produces findings using the same [file:line]: (severity) message [id] format, making them compatible with existing CI pipelines.
Cppcheck SARIF Output and GitHub Code Scanning
SARIF (Static Analysis Results Interchange Format) is the standard format GitHub Code Scanning accepts, making it straightforward to surface cppcheck findings directly in pull request reviews under the Security tab.
# Generate SARIF output
cppcheck \
--template=sarif \
--output-file=results.sarif \
--project=build/compile_commands.json \
--enable=warning,performance,portability \
--suppress=missingIncludeSystemUpload the SARIF file from a GitHub Actions workflow so findings appear inline on PRs:
# .github/workflows/cppcheck.yml
name: Cppcheck
on: [pull_request]
jobs:
cppcheck:
runs-on: ubuntu-24.04
permissions:
security-events: write # required to upload SARIF
steps:
- uses: actions/checkout@v4
- name: Install cppcheck
run: sudo apt-get install -y cppcheck cmake ninja-build
- name: Configure (export compile_commands)
run: |
cmake -B build -G Ninja \
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
-DCMAKE_BUILD_TYPE=Debug
- name: Run cppcheck
run: |
cppcheck \
--template=sarif \
--output-file=results.sarif \
--project=build/compile_commands.json \
--enable=warning,performance,portability \
--suppress=missingIncludeSystem \
--error-exitcode=1
- name: Upload SARIF to Code Scanning
if: always() # upload even when cppcheck exits non-zero
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: results.sarif
category: cppcheckThe if: always() on the upload step ensures findings reach the Security tab even when cppcheck reports errors and the step exits non-zero. Without it, a failed analysis produces no annotations on the PR.
Cppcheck Configuration for Large Codebases
At scale, a plain cppcheck src/ invocation becomes impractical — runtime climbs, incremental scanning becomes impossible, and the lack of a persistent config means every run needs the same long flags. The .cppcheck project file and several CLI flags address this.
.cppcheck project file
The .cppcheck XML project file captures paths, suppressions, and check options so IDEs (CLion, Qt Creator) and CI can share an identical configuration without repeating flags:
<?xml version="1.0" encoding="UTF-8"?>
<project version="1">
<builddir>build</builddir>
<analyze-all-vs-configs>false</analyze-all-vs-configs>
<paths>
<dir name="src"/>
<dir name="include"/>
</paths>
<exclude>
<path name="src/generated/"/>
<path name="third_party/"/>
</exclude>
<suppressions>
<suppression>missingIncludeSystem</suppression>
</suppressions>
<platforms>
<platform>unix64</platform>
</platforms>
</project># Use in CI — identical to what CLion runs
cppcheck --project=.cppcheck --error-exitcode=1Incremental scanning with --file-list
Run cppcheck only on files that changed by combining git diff with --file-list:
# Check only files changed in the current PR branch
git diff --name-only --diff-filter=ACM origin/main...HEAD \
| grep -E '\.(cpp|cxx|cc)$' \
> changed_files.txt
cppcheck --file-list=changed_files.txt \
--project=build/compile_commands.json \
--enable=warning,performanceParallel analysis and cross-TU depth
# Use all CPU cores
cppcheck --jobs=$(nproc) --project=build/compile_commands.json
# Cross-translation-unit (CTU) analysis — follows function calls across files
# Higher depth finds more bugs but increases runtime
cppcheck --project=build/compile_commands.json \
--enable=all \
--max-ctu-depth=10 \
--jobs=$(nproc)
# Check a library's API surface and generate hints about missing definitions
cppcheck --check-library --enable=information src/
# Produces: [src/util.cpp:12]: (information) --check-library: Function 'my_alloc' should have <noreturn> or allocation infoCTU analysis is disabled by default because it requires loading all translation units together. Start with --max-ctu-depth=4 and increase if the runtime is acceptable — beyond depth 10 the marginal gain diminishes sharply.
Custom Library Definitions (.cfg files)
Cppcheck doesn't compile your code — it analyses syntax and data flow directly. This means it has no way to know whether a custom function allocates memory, modifies its arguments, or always returns non-null unless you tell it. Library definition files (.cfg) provide those hints.
# Tell cppcheck which .cfg files to load
cppcheck --library=posix --library=gnu --library=myproject src/
# Discover what built-in libraries are available
ls /usr/share/cppcheck/cfg/
# gnu.cfg posix.cfg std.cfg windows.cfg bsd.cfg ...A .cfg file for a custom allocator pair:
<?xml version="1.0"?>
<def format="2">
<!-- my_malloc allocates uninitialised memory; my_free releases it -->
<memory>
<alloc init="false">my_malloc</alloc>
<dealloc>my_free</dealloc>
</memory>
<!-- my_strdup allocates initialised (string copy) memory -->
<memory>
<alloc init="true" bufferSize="strdup">my_strdup</alloc>
<dealloc>my_free</dealloc>
</memory>
<!-- my_open returns a resource handle; my_close releases it -->
<resource>
<alloc>my_open</alloc>
<dealloc>my_close</dealloc>
</resource>
<!-- flag that a function never returns (like abort()) -->
<function name="fatal_error">
<noreturn>true</noreturn>
</function>
</def>Without the <memory> block, cppcheck cannot track whether my_malloc return values are freed, so memleak checks are blind to your custom allocator. With it, cppcheck treats my_malloc/my_free identically to malloc/free.
# Use POSIX API awareness (open/close, pthread_mutex, etc.)
cppcheck --library=posix src/
# Use GNU extension awareness (__builtin_expect, __attribute__, etc.)
cppcheck --library=gnu src/
# Use your project's custom definitions
cppcheck --library=posix --library=myproject.cfg \
--project=build/compile_commands.json \
--enable=warning,performance \
--error-exitcode=1 \
src/Store .cfg files alongside the project (e.g. cppcheck/myproject.cfg) and reference them by path so CI and local runs stay in sync.
See Also
- clang-tidy — compiler-based linting with auto-fix and modernization checks
- PVS-Studio — commercial static analyzer with deeper interprocedural analysis
- static-analysis — overview of the C++ static analysis landscape and how to combine tools
- Address Sanitizer — runtime memory error detection to complement static checks