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

include-what-you-use (IWYU)

include-what-you-use tool — fixing redundant and missing includes, CMake integration, IWYU mappings, pragma directives, and CI integration.

TL;DR

include-what-you-use (IWYU) analyzes which #include directives are needed and suggests removing redundant ones and adding missing ones. It reduces compilation times and makes dependencies explicit.

bash
# Install
sudo apt install iwyu

# Run against a file
iwyu -std=c++20 main.cpp -- -I/path/to/headers

# Apply suggestions
iwyu_tool.py -p build/ | fix_include

Installation

bash
# Ubuntu/Debian
sudo apt install iwyu

# Fedora
sudo dnf install include-what-you-use

# macOS
brew install include-what-you-use

# From source (needs matching LLVM/Clang version)
git clone https://github.com/include-what-you-use/include-what-you-use
cd include-what-you-use && git checkout clang_17
cmake -S . -B build -DCMAKE_PREFIX_PATH=/usr/lib/llvm-17
cmake --build build

Basic Usage

bash
# Analyze a single file with compile commands
iwyu -std=c++20 -I./include main.cpp

# Example output:
# main.cpp should add these lines:
# #include <string>  // for std::string
# #include <vector>  // for std::vector
#
# main.cpp should remove these lines:
# - #include <iostream>  // lines 3-3
#
# The full include-list for main.cpp:
# #include <string>
# #include <vector>

CMake Integration

IWYU integrates via CMAKE_CXX_INCLUDE_WHAT_YOU_USE:

cmake
# CMakeLists.txt
find_program(IWYU NAMES include-what-you-use iwyu)
if(IWYU)
    set(CMAKE_CXX_INCLUDE_WHAT_YOU_USE
        ${IWYU}
        -Xiwyu --mapping_file=${CMAKE_SOURCE_DIR}/iwyu.imp
        -Xiwyu --no_fwd_decls
    )
endif()
bash
cmake -G Ninja -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build 2>&1 | grep -A 5 "should add\|should remove"

Or use iwyu_tool.py for a cleaner workflow:

bash
# Generate compile_commands.json first
cmake -G Ninja -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON

# Run IWYU over all files
iwyu_tool.py -p build/ -- -Xiwyu --mapping_file=iwyu.imp

# Apply the fixes automatically
iwyu_tool.py -p build/ | fix_includes --nosafe_headers

IWYU Mapping Files

Standard library headers don't always match the symbols you're using. A mapping file tells IWYU which header provides which symbol:

ini
# iwyu.imp — IWYU mapping file (JSON-ish format)
[
  # Use <string> not <basic_string.h>
  { "include": ["<bits/basic_string.h>", "private",
                 "<string>", "public"] },

  # Map Eigen symbols
  { "symbol": ["Eigen::Matrix", "private",
               "<Eigen/Core>", "public"] },

  # Forward declaration mapping
  { "symbol": ["std::ostream", "private",
               "<iosfwd>", "public"] }
]

IWYU Pragma Directives

Control IWYU behavior in source files with pragmas:

cpp
// Keep this include even if IWYU thinks it's unused
#include <some/header.h>  // IWYU pragma: keep

// Export this include — files that include this header
// don't need to include <vector> separately
#include <vector>  // IWYU pragma: export

// Associate a forward declaration with a full include
#include <iosfwd>  // IWYU pragma: no_include <ostream>

// Tell IWYU a file is an implementation detail
// (in pch.h or aggregate headers)
#include "impl/detail.h"  // IWYU pragma: private, include "public_api.h"

// Silence IWYU for a whole file
// IWYU pragma: no_include_details

Forward Declarations

IWYU suggests forward declarations when only a pointer or reference is used:

cpp
// widget.h — before IWYU
#include "dialog.h"  // only uses Dialog*

// widget.h — after IWYU (Dialog is only used as a pointer)
class Dialog;  // forward declaration — no full include needed

Disable this behavior if you want full includes everywhere:

bash
iwyu_tool.py -p build/ -- -Xiwyu --no_fwd_decls

CI Integration

yaml
# .github/workflows/iwyu.yml
name: IWYU Check

on: [push, pull_request]

jobs:
  iwyu:
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v4

      - name: Install dependencies
        run: |
          sudo apt-get install -y iwyu ninja-build cmake

      - name: Configure
        run: cmake -G Ninja -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON

      - name: Run IWYU
        run: |
          iwyu_tool.py -p build/ -- -Xiwyu --error_always 2>&1 | tee iwyu.log
          # Fail if any "should add/remove" lines
          ! grep -q "should add\|should remove" iwyu.log

Common Issues and Solutions

False positives from system headers:

bash
# Add the system mapping file
iwyu -Xiwyu --mapping_file=/usr/share/iwyu/gcc.libc.imp ...

Too many forward declaration suggestions:

bash
iwyu -Xiwyu --no_fwd_decls ...

PCH (precompiled headers) conflict:

cmake
# Disable IWYU for targets that use PCH
set_target_properties(mylib PROPERTIES
    CXX_INCLUDE_WHAT_YOU_USE "")

Boost headers (complex internal structure):

bash
# Use the Boost IWYU mappings
iwyu -Xiwyu --mapping_file=/path/to/boost.imp ...
# Available at: https://github.com/include-what-you-use/include-what-you-use/tree/master/mapfiles

Benefits vs Cost

BenefitDetail
Faster compilationFewer headers parsed per TU — can cut 30-50% on large projects
Explicit dependenciesMakes what each file actually needs clear
Fewer rebuild cascadesChanging impl.h doesn't rebuild files that only needed fwd.h
IDE performanceLess work for clangd / IntelliSense

Cost: Initial fix-up pass can be noisy (hundreds of suggestions on a legacy codebase). Run it incrementally on new files first.


Reading IWYU Output

Every line of IWYU output has a deliberate meaning — ignoring them leads to either bloated or broken translation units. Here is an annotated example for a file that uses std::vector, std::string, std::cout, and a third-party JSON header.

bash
# Run IWYU on a single file with a mapping file
iwyu_tool.py -p build/ -- -Xiwyu --mapping_file=iwyu.imp src/report.cpp

Typical output:

cpp
src/report.cpp should add these lines:
#include <string>   // for std::string
#include <vector>   // for std::vector

src/report.cpp should remove these lines:
- #include <algorithm>  // lines 4-4
- #include "utils/internal.h"  // lines 7-7

The full include-list for src/report.cpp:
#include <iostream>       // for std::cout, std::endl
#include <string>         // for std::string
#include <vector>         // for std::vector
#include "nlohmann/json.hpp"  // for nlohmann::json

"should add" means you use a symbol — std::vector, std::string — but you did not directly include the header that owns it. You were getting the definition transitively through another header, which is fragile: if that other header stops pulling it in, your build breaks silently. IWYU makes the dependency explicit.

"should remove" means you include a header but do not directly use anything it declares in this translation unit. <algorithm> might have been needed during an earlier refactor and is now dead weight — it costs compile time on every build. utils/internal.h is even worse: it couples your file to an internal API that may change.

"The full include-list" is the canonical result. Copy it verbatim (or let fix_includes do it) and your file will compile with exactly the headers it needs — no more, no less.


Incremental Adoption on Legacy Codebases

The "big bang" approach — running IWYU over an entire large codebase at once — produces hundreds of noisy suggestions and overwhelms reviewers. The practical strategy is to apply IWYU only to files you are already touching.

Run IWYU on changed files only, using the diff of the current branch:

bash
# Get all changed .cpp files since the last commit
git diff --name-only HEAD~1 | grep '\.cpp$' > changed_files.txt

# Run IWYU only on those files
iwyu_tool.py -p build/ $(cat changed_files.txt | sed 's/^/-- /') \
  -- -Xiwyu --mapping_file=iwyu.imp | fix_includes --nosafe_headers

For CI, scope IWYU to the PR diff so it never blocks on pre-existing violations:

yaml
# .github/workflows/iwyu-incremental.yml
name: IWYU Incremental

on: [pull_request]

jobs:
  iwyu:
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Install tools
        run: sudo apt-get install -y iwyu ninja-build cmake

      - name: Configure
        run: cmake -G Ninja -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON

      - name: Run IWYU on changed files only
        run: |
          CHANGED=$(git diff --name-only origin/${{ github.base_ref }}...HEAD \
                    | grep '\.cpp$' | tr '\n' ' ')
          if [ -z "$CHANGED" ]; then exit 0; fi
          iwyu_tool.py -p build/ $CHANGED -- -Xiwyu --error_always 2>&1 | tee iwyu.log
          ! grep -q "should add\|should remove" iwyu.log

You can also check a single new file explicitly using --check_also:

bash
# Only check src/newfeature.cpp, regardless of what else is in the build
iwyu_tool.py -p build/ -- -Xiwyu --check_also=src/newfeature.cpp \
  | fix_includes --nosafe_headers

This approach lets you enforce IWYU hygiene on all new code without requiring a clean-up of the entire legacy tree before you can merge.


IWYU and Modules (C++20)

IWYU does not yet understand C++20 module imports — it was designed for the #include model and has no awareness of import declarations. If your project is transitioning to modules, you need to understand the boundary carefully.

IWYU will not analyze, add, or remove import statements:

cpp
// IWYU ignores this entirely — it does not know what symbols
// "import std;" or "import mylib;" export
import std;
import mylib;

// IWYU does analyze these normally
#include <cassert>
#include "legacy/util.h"

In a hybrid project — where some translation units use import and others still use #include — running IWYU on the #include-based files remains fully safe and beneficial. The risk is in mixed TUs: if a file does import std; and also #include <vector>, IWYU may suggest removing <vector> because it sees the import as satisfying nothing (it doesn't parse module interfaces). Guard those includes with // IWYU pragma: keep.

cpp
// Hybrid TU: keep the include for non-modular consumers of this header
#include <vector>   // IWYU pragma: keep — still needed by #include consumers
import mylib;

Until IWYU ships module-aware analysis, the safe rule is: do not run fix_includes on TUs that contain import declarations.


Custom Mapping Files Deep Dive

Mapping files are what make IWYU usable on real projects — without them, IWYU gives you technically correct but practically wrong suggestions (e.g., include <bits/stl_vector.h> instead of <vector>).

There are three kinds of mappings:

1. include-to-include — redirect a private/internal header to the public one:

ini
# iwyu.imp
[
  { "include": ["<bits/stl_vector.h>", "private", "<vector>", "public"] },
  { "include": ["<bits/basic_string.h>", "private", "<string>", "public"] },
  { "include": ["\"mylib/internal/impl.h\"", "private",
                 "\"mylib/mylib.h\"", "public"] }
]

2. symbol-to-include — declare which header is the authoritative source for a symbol:

ini
[
  { "symbol": ["std::vector", "private", "<vector>", "public"] },
  { "symbol": ["Eigen::MatrixXd", "private", "<Eigen/Dense>", "public"] },
  { "symbol": ["nlohmann::json", "private", "<nlohmann/json.hpp>", "public"] }
]

3. symbol-to-symbol — prefer a forward declaration over a full include when only a pointer is needed:

ini
[
  { "symbol": ["std::ostream", "private", "<iosfwd>", "public"] },
  { "symbol": ["MyWidget", "private", "\"widgets/fwd.h\"", "public"] }
]

The IWYU repository ships ready-made mapping files for common libraries in its mapfiles/ directory. Use them instead of writing your own from scratch:

FileCovers
boost.impBoost headers (complex internal routing)
gtest.impGoogle Test / Google Mock
libcxx.implibc++ internal-to-public header mapping
qt5_4.impQt 5.4+ module headers
bash
# Point IWYU at multiple mapping files
iwyu_tool.py -p build/ -- \
  -Xiwyu --mapping_file=/usr/share/iwyu/mapfiles/boost.imp \
  -Xiwyu --mapping_file=./project.imp

For your own library's private/public split, add entries to a project-local .imp file and commit it alongside the code. New contributors inherit correct IWYU behavior immediately.


Combining IWYU with clangd

clangd has built-in IWYU-like suggestions, and the two tools complement each other rather than overlap. Understanding the difference determines when to use each.

clangd's include insertion activates at completion time — as you type std::vector, clangd can offer to add #include <vector> for you. Enable it in .clangd:

yaml
# .clangd
CompileFlags:
  Add: [-std=c++20]

Diagnostics:
  MissingIncludes: Strict
  UnusedIncludes: Strict

InlayHints:
  Enabled: No

With MissingIncludes: Strict, clangd underlines uses of symbols whose owning header is not directly included — essentially real-time IWYU feedback in your editor. UnusedIncludes: Strict flags includes that contribute nothing to the current file.

The key difference is scope: clangd operates on the file open in your editor right now; IWYU is a batch audit tool that processes every file in your build database in one pass. Use clangd to stay clean as you write new code, and use IWYU in CI to enforce cleanliness across the whole codebase.

bash
# clangd respects the same compile_commands.json as IWYU
cmake -G Ninja -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
ln -s build/compile_commands.json compile_commands.json
# Now both clangd (editor) and iwyu_tool.py (CI) see identical flags

Pass the same --mapping_file arguments to both tools so their suggestions agree — nothing is more confusing than clangd suggesting <vector> while IWYU suggests <bits/stl_vector.h>.


IWYU Output as a Compilation-Time Metric

Every unnecessary #include is a hidden tax on every developer's build. A header included in 200 files that pulls in 40 transitive headers causes 200 × 40 = 8,000 extra file parses on a cold build. IWYU cleanup directly shrinks that number.

Use clang -H to visualize the transitive include tree before and after cleanup:

bash
# Show all headers recursively included by a single file
clang++ -std=c++20 -H -fsyntax-only src/report.cpp 2>&1 | head -60

# Count total unique headers pulled in
clang++ -std=c++20 -H -fsyntax-only src/report.cpp 2>&1 \
  | grep '^\.' | sed 's/^\.*//;s/ .*//' | sort -u | wc -l

Track this number in CI as a health metric. A file with 10 direct includes that expands to 50 transitive headers is a warning sign. After an IWYU pass on a large codebase:

Codebase sizeTypical include-depth reductionCold build improvement
100K LOC30–40% fewer transitive includes20–30% faster
500K LOC40–55% fewer transitive includes30–40% faster
1M+ LOC50–65% fewer transitive includes40–50% faster

These numbers reflect real-world reports from projects like Chromium, LLVM, and large financial systems that have applied systematic IWYU cleanup. The single highest-leverage target is usually a widely-included "utility" header that quietly drags in <string>, <vector>, <map>, and friends even in files that need none of them.

bash
# Find your most-included header (the one most worth cleaning up)
grep -r '#include' src/ | grep -o '"[^"]*"' | sort | uniq -c | sort -rn | head -20

See Also

  • clang-tidy — static analysis and bug-finding; pairs well with IWYU in CI
  • clangd — language server with real-time include suggestions that complement IWYU's batch audits
  • clang-format — formatting tool; run after fix_includes to normalize include ordering
  • CMake — build system used for compile_commands.json generation and CMAKE_CXX_INCLUDE_WHAT_YOU_USE integration
Edit on GitHubUpdated 2025-01-01T00:00:00.000Z