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

gcov / lcov

Code coverage for C++ — gcov instruments GCC/Clang builds, lcov generates HTML reports, and llvm-cov serves the Clang-native alternative.

TL;DR

gcov measures which lines and branches your tests execute. Compile with -fprofile-arcs -ftest-coverage (or --coverage), run your tests, then use lcov + genhtml to get an HTML report. For Clang, use llvm-cov with -fprofile-instr-generate -fcoverage-mapping instead.

bash
# Compile with coverage
g++ -std=c++20 --coverage -O0 -o myapp_tests tests.cpp src/lib.cpp

# Run tests (generates .gcda files)
./myapp_tests

# Generate HTML report
lcov --capture --directory . --output-file coverage.info
lcov --remove coverage.info '/usr/*' '*/tests/*' --output-file coverage.info
genhtml coverage.info --output-directory coverage_html

# Open
open coverage_html/index.html

GCC coverage (gcov)

Compilation flags

bash
# Long form
g++ -fprofile-arcs -ftest-coverage -O0 main.cpp -o myapp

# Short form (equivalent)
g++ --coverage -O0 main.cpp -o myapp

Always use -O0 with coverage — optimizations merge and reorder lines, making coverage data misleading.

What gets generated

After running your tests, two types of files appear next to each .o:

  • .gcno — control-flow graph (created at compile time)
  • .gcda — arc counts (created at runtime, updated each test run)
bash
# View coverage for a single file
gcov -r src/mylib.cpp

# Output: mylib.cpp.gcov with line-by-line counts

lcov workflow

bash
# Install
sudo apt install lcov

# Collect coverage from all .gcda files
lcov --capture --directory . --output-file coverage.info

# Remove system headers and test files from report
lcov --remove coverage.info \
  '/usr/*' \
  '*/googletest/*' \
  '*/tests/*' \
  --output-file coverage.info

# Print summary
lcov --list coverage.info

# Generate HTML report
genhtml coverage.info \
  --output-directory coverage_html \
  --title "My Project" \
  --legend

# Open
xdg-open coverage_html/index.html

Branch coverage

bash
# Enable branch coverage (line + branch)
g++ --coverage -fprofile-arcs -fbranch-probabilities -O0 -o myapp tests.cpp

# In lcov
lcov --capture --directory . --rc lcov_branch_coverage=1 --output-file coverage.info
genhtml --branch-coverage coverage.info --output-directory coverage_html

Clang coverage (llvm-cov)

Clang has its own coverage implementation that's more accurate and faster:

bash
# Compile
clang++ -std=c++20 -fprofile-instr-generate -fcoverage-mapping -O0 -o myapp_tests tests.cpp

# Run (generates default.profraw)
LLVM_PROFILE_FILE="myapp.profraw" ./myapp_tests

# Merge profile data
llvm-profdata merge -sparse myapp.profraw -o myapp.profdata

# Show coverage summary
llvm-cov report ./myapp_tests -instr-profile=myapp.profdata

# Show per-file line coverage
llvm-cov show ./myapp_tests -instr-profile=myapp.profdata -format=html -output-dir=coverage_html

# Export as lcov format (for CI tools that expect lcov)
llvm-cov export ./myapp_tests -instr-profile=myapp.profdata -format=lcov > coverage.info

CMake integration

cmake
# Add a coverage build type
option(ENABLE_COVERAGE "Enable coverage reporting" OFF)

if(ENABLE_COVERAGE)
  if(CMAKE_CXX_COMPILER_ID MATCHES "GNU")
    target_compile_options(myapp_tests PRIVATE --coverage -O0)
    target_link_options(myapp_tests PRIVATE --coverage)
  elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
    target_compile_options(myapp_tests PRIVATE
      -fprofile-instr-generate -fcoverage-mapping -O0)
    target_link_options(myapp_tests PRIVATE -fprofile-instr-generate)
  endif()
endif()
bash
cmake -B build -DENABLE_COVERAGE=ON -DCMAKE_BUILD_TYPE=Debug
cmake --build build
cd build && ctest
# Then run lcov/llvm-cov as above

CI integration (GitHub Actions)

yaml
- name: Build with coverage
  run: |
    cmake -B build -DENABLE_COVERAGE=ON -DCMAKE_BUILD_TYPE=Debug
    cmake --build build

- name: Run tests
  run: ctest --test-dir build -V

- name: Generate coverage report
  run: |
    lcov --capture --directory build --output-file coverage.info
    lcov --remove coverage.info '/usr/*' '*/tests/*' --output-file coverage.info

- name: Upload to Codecov
  uses: codecov/codecov-action@v4
  with:
    files: coverage.info
    fail_ci_if_error: true

Coverage targets and what they mean

MetricMeaningRealistic target
Line coverage% of lines executed80-90% for libs
Branch coverage% of if/else branches taken70-80%
Function coverage% of functions called90%+

100% coverage is not the goal. Coverage shows what you didn't test — it doesn't validate that what you tested is correct. Focus on:

  1. Testing error paths (often the least covered)
  2. Testing boundary conditions
  3. Testing exception handling

Exclude from coverage:

  • Debug/logging code
  • Platform-specific fallback code unlikely to run in CI
  • main() and test harness code
bash
# Exclude via annotation (GCC/Clang)
/* LCOV_EXCL_START */
void debugDumpState() { /* ... */ }
/* LCOV_EXCL_STOP */

// Single line
int x = debug ? 1 : 0;  // LCOV_EXCL_LINE
Edit on GitHubUpdated 2026-05-01T00:00:00.000Z