Contact
Evil Genius SocietyHomeLearnForumLeaderboardSocialsMessages
← Back to module

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:

  1. A header includes only what it needs to compile itself. Everything else moves to the .cpp.
  2. A .cpp includes everything it uses, even if it would arrive transitively. Relying on engine.hpp to bring you <string> means the day someone cleans up engine.hpp, your file breaks for no reason connected to your change.
  3. Include your own header first in each .cpp. If engine.cpp starts 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

  • #include is textual, so a header reached by two paths is pasted twice — a redefinition error.
  • Include guards (#ifndef/#define/#endif) or #pragma once make the second paste a no-op.
  • #pragma once is not standard but is supported by GCC, Clang and MSVC.
  • Duplicated guard macros make a header silently expand to nothing; #pragma once cannot collide.
  • Guards stop double inclusion; they do nothing about a header being enormous.
  • Measured: engine_fat.hpp preprocesses 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 declare std::ostream& without <iostream>.
  • A header includes what it needs to compile alone; check with g++ -fsyntax-only header.hpp.
  • A .cpp includes 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.