Skip to content
C++
Analysis & Formatting
Updated 2025-01-01T00:00:00.000Z

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

bash
# 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 build

Basic Usage

bash
# 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

cpp
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

bash
# 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

cpp
// 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;
bash
# Suppression file (suppressions.txt)
# nullPointer:src/legacy.cpp:42
# uninitvar

cppcheck --suppressions-list=suppressions.txt src/

CMake Integration

cmake
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)

yaml
- 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.xml

Cppcheck vs clang-tidy

cppcheckclang-tidy
Requires compilationNoYes (compile_commands.json)
Bug findingStrongStrong
Code modernizationNoYes (modernize-* checks)
Performance checksBasicStrong
False positive rateLowMedium
SpeedFastSlower
Use together?Yes — complementaryYes

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.

bash
# 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=1

The --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.

bash
# 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.vcxproj

Symlink the database to the project root so editors and other tools find it automatically:

bash
ln -sf build/compile_commands.json compile_commands.json

Common 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

cpp
// 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

cpp
// 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

cpp
// 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

cpp
// 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

cpp
// 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 branch

useStlAlgorithm — prefer std::for_each

cpp
// 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

cpp
// 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

cpp
// 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 variable

Cppcheck 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.

bash
# 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.

bash
# 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.json

Addons 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.

bash
# Generate SARIF output
cppcheck \
  --template=sarif \
  --output-file=results.sarif \
  --project=build/compile_commands.json \
  --enable=warning,performance,portability \
  --suppress=missingIncludeSystem

Upload the SARIF file from a GitHub Actions workflow so findings appear inline on PRs:

yaml
# .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: cppcheck

The 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
<?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>
bash
# Use in CI — identical to what CLion runs
cppcheck --project=.cppcheck --error-exitcode=1

Incremental scanning with --file-list

Run cppcheck only on files that changed by combining git diff with --file-list:

bash
# 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,performance

Parallel analysis and cross-TU depth

bash
# 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 info

CTU 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.

bash
# 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
<?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.

bash
# 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
Edit on GitHubUpdated 2025-01-01T00:00:00.000Z