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.
# 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.htmlGCC coverage (gcov)
Compilation flags
# Long form
g++ -fprofile-arcs -ftest-coverage -O0 main.cpp -o myapp
# Short form (equivalent)
g++ --coverage -O0 main.cpp -o myappAlways 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)
# View coverage for a single file
gcov -r src/mylib.cpp
# Output: mylib.cpp.gcov with line-by-line countslcov workflow
# 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.htmlBranch coverage
# 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_htmlClang coverage (llvm-cov)
Clang has its own coverage implementation that's more accurate and faster:
# 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.infoCMake integration
# 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()cmake -B build -DENABLE_COVERAGE=ON -DCMAKE_BUILD_TYPE=Debug
cmake --build build
cd build && ctest
# Then run lcov/llvm-cov as aboveCI integration (GitHub Actions)
- 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: trueCoverage targets and what they mean
| Metric | Meaning | Realistic target |
|---|---|---|
| Line coverage | % of lines executed | 80-90% for libs |
| Branch coverage | % of if/else branches taken | 70-80% |
| Function coverage | % of functions called | 90%+ |
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:
- Testing error paths (often the least covered)
- Testing boundary conditions
- Testing exception handling
Exclude from coverage:
- Debug/logging code
- Platform-specific fallback code unlikely to run in CI
main()and test harness code
# Exclude via annotation (GCC/Clang)
/* LCOV_EXCL_START */
void debugDumpState() { /* ... */ }
/* LCOV_EXCL_STOP */
// Single line
int x = debug ? 1 : 0; // LCOV_EXCL_LINE