Lesson 3 of 10
Lesson 17.2 established what may go in a header. This lesson is about two failures of the mechanism itself: including the same header twice in one translation unit, and including far more than you needed.
The first is a compile error you will hit on day one. The second costs you minutes on every build for years and never produces an error at all.
The double-inclusion problem
// point.hpp
struct Point { int x, y; };
// shape.hpp
#include "point.hpp"
struct Shape { Point origin; };
// main.cpp
#include "point.hpp"
#include "shape.hpp"
int main() { Shape s{}; return s.origin.x; }
main.cpp includes point.hpp directly, and again through shape.hpp. Since #include is a
textual paste, struct Point appears twice in one translation unit:
g++ -std=c++20 -c main.cpp -o main.o
In file included from shape.hpp:2,
from main.cpp:3:
point.hpp:2:8: error: redefinition of ‘struct Point’
2 | struct Point { int x, y; };
| ^~~~~
In file included from main.cpp:2:
point.hpp:2:8: note: previous definition of ‘struct Point’
2 | struct Point { int x, y; };
| ^~~~~
Nothing here is unusual — two headers in a project both needing a third is the normal case, not a mistake. The fix is to make the header ignore its second paste.
Two spellings
Include guards are the portable form, and they are just #if:
// point.hpp
#ifndef POINT_HPP
#define POINT_HPP
struct Point { int x, y; };
#endif // POINT_HPP
The first paste defines POINT_HPP; the second finds it already defined and skips to #endif.
#pragma once does the same thing in one line:
// point.hpp
#pragma once
struct Point { int x, y; };
g++ -std=c++20 -c main.cpp -o main.o && echo "compiles now"
compiles now
It is not in the C++ standard, but GCC, Clang and MSVC all support it, which is every compiler most projects will meet. It also cannot suffer the one real failure of hand-written guards:
// widget.hpp
#ifndef UTIL_HPP // copy-pasted from another header and not updated
#define UTIL_HPP
struct Widget { int id; };
#endif
If two headers share a guard macro, the second one to be included silently expands to nothing,
and you get an error about an undefined type somewhere else entirely. #pragma once keys on the
file itself, so it cannot collide.
Use #pragma once unless your project forbids it. If you use guards, derive the macro from the full
path — EGS_GEOMETRY_POINT_HPP, not POINT_HPP.
The expensive problem
Guards stop a header being pasted twice. They do nothing about a header being enormous.
Here is a header that needs Heavy only as a pointer and a reference — it never looks inside one:
// heavy.hpp
#pragma once
#include <iostream>
#include <map>
#include <regex>
#include <string>
#include <vector>
struct Heavy {
std::map<std::string, std::vector<int>> data;
};
// engine_fat.hpp
#pragma once
#include "heavy.hpp"
class EngineFat {
Heavy* impl_;
public:
void run(const Heavy& h);
};
// engine_thin.hpp
#pragma once
// A forward declaration is enough: this header only names Heavy, it never
// looks inside one.
struct Heavy;
class EngineThin {
Heavy* impl_;
public:
void run(const Heavy& h);
};
The two classes are identical. The headers are not:
for h in engine_fat engine_thin; do
echo "#include \"$h.hpp\"" > probe.cpp
g++ -std=c++20 -E probe.cpp -o probe.i
printf '%-18s %7s preprocessed lines\n' "$h.hpp" "$(wc -l < probe.i)"
done
engine_fat.hpp 79019 preprocessed lines
engine_thin.hpp 20 preprocessed lines
79,019 lines against 20. Nearly four thousand times the text, for a class declaration that is the same either way.
And that cost is paid per translation unit. Eight files including each version:
for i in $(seq 1 8); do
printf '#include "engine_fat.hpp"\nvoid fat%d(EngineFat&) {}\n' $i > fat$i.cpp
printf '#include "engine_thin.hpp"\nvoid thin%d(EngineThin&) {}\n' $i > thin$i.cpp
done
timeit() {
local s=$(date +%s%N)
for i in $(seq 1 8); do g++ -std=c++20 -c $1$i.cpp -o $1$i.o; done
local e=$(date +%s%N)
echo $(( (e-s)/1000000 ))
}
fat=$(timeit fat)
thin=$(timeit thin)
echo "fat at least 10x slower than thin: $([ $((fat / thin)) -ge 10 ] && echo yes || echo no)"
fat at least 10x slower than thin: yes
The run that produced this lesson measured 3,968 ms against 121 ms — a factor of 33. The exact
figures depend on the machine, which is why the check above asserts only the robust claim, but the
order of magnitude is not subtle and it is not a microbenchmark artifact. It is eight files
preprocessing <regex> for no reason.
When a forward declaration is enough
You need the full definition when the compiler must know the type's size or members:
- A member by value:
Heavy h; - Inheriting from it:
class E : public Heavy - Calling anything on it, or accessing a member
sizeof, or putting it in a container by value
A forward declaration suffices when the type is only named:
- Pointers and references as members:
Heavy* p;,Heavy& r; - Function parameters and return types in declarations
std::unique_ptr<Heavy>members — with a caveat below
The rule of thumb: if your header would still compile with the type's definition deleted, forward declare it.
The unique_ptr caveat is worth knowing because it catches people. A std::unique_ptr<Heavy>
member is fine with a forward declaration, but the destructor of the owning class needs Heavy to
be complete, and an implicit destructor is generated wherever it is first needed — usually in a
header, where Heavy is still incomplete. The fix is to declare ~EngineThin(); in the header and
define it in the .cpp, after including heavy.hpp. That is exactly the Pimpl idiom, and
lesson 19.2 builds it properly.
Where the includes belong
The discipline that follows from all of this:
- A header includes only what it needs to compile itself. Everything else moves to the
.cpp. - A
.cppincludes everything it uses, even if it would arrive transitively. Relying onengine.hppto bring you<string>means the day someone cleans upengine.hpp, your file breaks for no reason connected to your change. - Include your own header first in each
.cpp. Ifengine.cppstarts with#include "engine.hpp", the header is proved self-sufficient on every build. Put it last and a missing include inside it can hide for years.
Point 3 is a one-line habit that removes an entire category of bug, and it is the only one of the three that costs nothing to adopt.
Try it
Take a header that includes four standard headers to support one class, and split it: forward
declare what you can, move the rest into the .cpp. Measure preprocessed lines before and after,
then prove the header is self-sufficient by compiling it alone. Finally, demonstrate the
transitive-include trap — a .cpp that compiles only because some other header happened to
include <string>.
Checkpoint
#includeis textual, so a header reached by two paths is pasted twice — a redefinition error.- Include guards (
#ifndef/#define/#endif) or#pragma oncemake the second paste a no-op. #pragma onceis not standard but is supported by GCC, Clang and MSVC.- Duplicated guard macros make a header silently expand to nothing;
#pragma oncecannot collide. - Guards stop double inclusion; they do nothing about a header being enormous.
- Measured:
engine_fat.hpppreprocesses to 79,019 lines, the forward-declaring version to 20. - Measured: eight translation units, 3,968 ms against 121 ms — a factor of 33.
- Use a forward declaration when the header only names a type: pointers, references, and parameter types in declarations.
- Use the full definition for members by value, inheritance, member access, and
sizeof. std::unique_ptr<T>to an incomplete type needs an out-of-line destructor — that is Pimpl (lesson 19.2).<iosfwd>exists precisely so a header can declarestd::ostream&without<iostream>.- A header includes what it needs to compile alone; check with
g++ -fsyntax-only header.hpp. - A
.cppincludes what it uses, even if it would arrive transitively. - Include your own header first in each
.cpp— it proves the header self-sufficient on every build.
Questions about this lesson
Log in to ask a question.