Question
A C or C++ source file can accumulate more and more #include directives over time as code is moved or refactored. Some headers may no longer be needed, which can increase compilation time and create unnecessary build dependencies.
Is there a way to detect superfluous #include directives and identify which ones can be safely removed? Can a linter or compiler help with this?
Short Answer
You will learn why unused includes matter, how C and C++ include dependencies work, how to verify whether an include is needed, and how tools such as Include What You Use can assist. You will also learn why successful compilation alone is not always enough to prove that a header has the correct direct dependencies.
Concept
#include is handled by the C/C++ preprocessor before compilation. Conceptually, an include directive copies the contents of another file into the current translation unit.
#include <vector>
After preprocessing, the compiler sees the declarations from vector as if they appeared in your source file.
An include is directly required when the file uses a declaration that the included header provides. For example, code that declares a std::vector<int> needs access to the declaration of std::vector, so it should include <vector>.
#include <vector>
int main() {
std::vector<int> scores{10, 20, 30};
}
An include is potentially superfluous when removing it does not affect the file's required declarations or definitions.
However, there is an important complication: code can compile because another header includes the declaration indirectly. That is called a transitive include.
#include "report.hpp" // Might currently include <string> internally
std::string makeLabel();
This may compile today, but report.hpp is not necessarily supposed to provide std::string to its callers. If report.hpp later stops including <string>, this source file breaks. Therefore, a robust rule is:
Include the headers that directly provide the declarations your file uses; do not rely on incidental transitive includes.
This practice reduces unnecessary dependencies while keeping each file stable when unrelated headers change.
Mental Model
Think of each #include as a document placed into a folder before someone reviews it.
- A needed include is a document the reviewer must read to understand a name used in the folder.
- An unused include is an extra document that makes the folder larger without helping.
- A transitive include is when one document happens to contain a copy of another document you needed.
Relying on that copied document is fragile. If someone removes the copy later, your folder is suddenly incomplete. The safest approach is to put the document you directly need into the folder yourself.
Syntax and Examples
Use standard-library headers for the standard-library types and functions that your file directly uses.
#include <iostream>
#include <string>
#include <vector>
int main() {
std::vector<std::string> names{"Ada", "Linus"};
for (const std::string& name : names) {
std::cout << name << '\n';
}
}
Why each include is present:
<vector>providesstd::vector.<string>providesstd::string.<iostream>providesstd::cout.
If the program does not use std::vector, <vector> is a candidate for removal. Remove it, compile the relevant target, and keep it removed if the file still has all of its required direct dependencies.
For project headers, include the header that declares the type or function you use.
#include "customer.hpp"
void printCustomer(const Customer& customer);
If this file only needs to declare a function taking Customer by reference, a forward declaration may be sufficient instead:
class Customer;
void printCustomer(const Customer& customer);
Whether this is appropriate depends on the project's header design and on where the code needs the complete Customer definition.
Step by Step Execution
Consider this source file:
#include <iostream>
#include <string>
#include <vector>
int main() {
std::string message = "Build complete";
std::cout << message << '\n';
}
Step by step:
- The preprocessor expands
<iostream>,<string>, and<vector>before the compiler processesmain. - The compiler sees
std::string, so<string>is required. - The compiler sees
std::cout, so<iostream>is required. - The compiler never sees
std::vectoror another declaration provided specifically for this source file by<vector>. - Remove
<vector>:
#include <iostream>
#include <string>
int main() {
std::string message = "Build complete";
std::cout << message << '\n';
}
- Rebuild the target. It should behave identically, with one less direct dependency.
For a header file, also test that the header can be included on its own. This catches headers that accidentally depend on a previous include.
Real World Use Cases
- Faster incremental builds: Removing expensive, unneeded headers can reduce parsing work across many source files.
- More reliable refactoring: A file with explicit direct dependencies is less likely to break when another header is reorganized.
- Library interfaces: Public headers should avoid exposing unnecessary dependencies to every application that includes them.
- Reducing rebuild cascades: If a source file includes fewer project headers, changes to unrelated components trigger fewer recompilations.
- Code reviews: Include cleanup makes a file's actual dependencies easier for reviewers to understand.
- Large monorepos: Automated include analysis helps teams find dependency problems that are tedious to inspect manually.
Real Codebase Usage
In real C++ projects, include cleanup is usually part of dependency hygiene rather than a one-time task.
Include what you use
A common policy is: every .cpp or .hpp file includes the declarations it directly needs. For standard-library names, include the corresponding standard header. For project types, include the project header that owns the declaration.
Keep public headers self-contained
A public header should usually compile when included first in a small test translation unit:
#include "widget.hpp"
int main() {}
If this fails, widget.hpp may be relying on a header included earlier by its caller.
Use forward declarations deliberately
Headers often use forward declarations to avoid including large headers when only pointers or references are needed.
// session.hpp
class Database;
class Session {
public:
explicit Session(Database& database);
private:
Database& database_;
};
The implementation file needs the complete type when it calls Database members:
// session.cpp
#include "session.hpp"
#include "database.hpp"
Session::Session(Database& database) : database_(database) {}
Use tooling as a review aid
Include What You Use (often called IWYU) is a C/C++ analysis tool that can report missing direct includes and suggest removals or replacements. It is useful for generating suggestions, but its output should be reviewed and validated by building and testing the affected targets.
Compilers can also reveal the include tree. GCC and Clang support -H to print headers as they are included, which helps investigate unexpectedly large dependency chains.
clang++ -H -c app.cpp
Automate verification
Teams commonly run a normal build and tests after include changes. For public headers, they may also add include-self tests or lint rules in continuous integration.
Common Mistakes
Removing an include only because the current build succeeds
This can be correct, but check whether the file is now relying on a transitive include.
// Fragile: this compiles only if another header currently includes <string>.
#include "config.hpp"
std::string readName();
Prefer the direct dependency:
#include <string>
#include "config.hpp"
std::string readName();
Including a header only because a type appears as a pointer or reference
A complete definition is often unnecessary in a header for pointers and references.
// Potentially heavier than necessary in a header.
#include "engine.hpp"
class Car {
Engine* engine_;
};
Often, this can be written as:
class Engine;
class Car {
Engine* engine_;
};
The .cpp file still needs #include "engine.hpp" before it uses Engine members or destroys an owning smart pointer in contexts requiring a complete type.
Forward-declaring types that require a complete definition
A forward declaration is not enough when the compiler needs the object's size or members.
class Engine;
class Car {
Engine engine_; // Error: the size of Engine is needed here.
};
Include engine.hpp when storing an object by value, inheriting from it, accessing members, or otherwise requiring its complete definition.
Removing a header needed by a macro or platform-specific build path
Some dependencies are used only under #if conditions, in templates, or on a different operating system. Build the relevant configurations rather than validating only one local build.
Treating tool suggestions as unquestionable
Static analysis can be affected by compiler flags, generated headers, macros, and project-specific conventions. Review suggestions, then compile and test.
Forgetting that include order can hide problems
This may compile:
#include <string>
#include "profile.hpp"
even if profile.hpp itself uses std::string but forgets to include <string>. Test headers independently or place the project header first in implementation files when that matches your project's convention.
Comparisons
| Technique | Best use | Main limitation |
|---|---|---|
| Manually remove an include and rebuild | Small, isolated cleanup | Tedious across many files; can miss alternate build configurations |
Compiler include tree (-H) | Investigating why a file pulls in many headers | Shows what is included, not whether each include is semantically necessary |
| Include What You Use analysis | Finding missing, replaceable, and potentially removable includes | Suggestions need project-aware review and build validation |
| Forward declarations | Reducing header dependencies for pointers and references | Cannot replace a full definition when size, members, or inheritance are needed |
| Relying on transitive includes | May appear convenient temporarily | Fragile; unrelated header changes can break the file |
A useful distinction is between unused and indirectly available:
- An unused include contributes nothing the file needs and can usually be removed.
- An indirectly available declaration is currently supplied by another header, but the file may still need to add its own direct include for correctness and maintainability.
Cheat Sheet
#includeis processed before C/C++ compilation.- Include the header that directly declares each type, function, or template you use.
- Remove includes that provide nothing used by the file.
- Do not rely on headers included indirectly by other headers.
- Rebuild the affected target after every include cleanup.
- Run tests and relevant build configurations, especially when macros or platform code are involved.
- Use
clang++ -H -c file.cpporg++ -H -c file.cppto inspect the include tree. - Use Include What You Use (IWYU) for analysis suggestions; review its output.
- Use forward declarations in headers when only a pointer or reference to a class is required.
- Include the full type definition when storing by value, inheriting, accessing members, or otherwise needing the type's layout.
- Ensure public headers are self-contained: they should compile when included without relying on prior includes.
FAQ
Can a compiler automatically warn about every unused #include?
Generally, no standard compiler warning precisely and safely identifies every removable include. C++ preprocessing, macros, conditional compilation, templates, and transitive includes make the problem project-dependent.
What tool can find unused C++ includes?
Include What You Use (IWYU) is a commonly used C/C++ analysis tool that suggests includes to add, remove, or replace. Treat its output as suggestions and verify them with your actual build and tests.
Does clang++ -H find unused headers?
No. -H prints the include hierarchy. It helps you see what is being included and how deeply, but it does not determine semantic necessity.
Is it safe to remove an include if the file still compiles?
It may be safe, but check whether the file is accidentally depending on a transitive include. A direct include for every directly used declaration is more resilient to future refactoring.
Should every C++ header include all headers it uses?
Usually yes for declarations required by the header's own public contents. This makes the header self-contained. Use forward declarations where they are valid and reduce unnecessary coupling.
Can I forward-declare every class instead of including its header?
No. Forward declarations work only where an incomplete type is allowed, such as with pointers, references, and some function declarations. Objects stored by value require the complete type.
Why do unused includes slow builds?
Every included header may pull in many other headers. The compiler then parses more declarations and templates for each translation unit, increasing work and potentially causing more files to rebuild after changes.
Mini Project
Description
Clean up the dependencies of a small C++ program that formats user names. The program deliberately contains an unused include so you can practice identifying and removing it while retaining the headers required by the code.
Goal
Produce a program with only the direct standard-library includes needed for its current implementation.
Requirements
Identify which standard-library facilities the program uses.
Remove the include that is not required.
Keep the program compiling with a C++ compiler.
Ensure the output remains Hello, Ada!.
Explain why each remaining include is needed.
Keep learning
Related questions
2D Array Loop Order and Cache Performance in C
Learn why swapping nested loops changes 2D array performance in C, using row-major memory layout, cache locality, and practical benchmarks.
Array-to-Pointer Conversion in C and C++ Explained
Learn what array-to-pointer conversion means in C and C++, how array decay works, and how it differs from a pointer to an array.
Building More Fault-Tolerant Embedded C++ Applications for Radiation-Prone ARM Systems
Learn practical C++ and compile-time techniques to reduce soft-error damage in embedded ARM systems exposed to radiation.