The C/C++ Compilation Model

How a translation unit becomes an object file and why the include graph, not the source line count, determines your build time. Covers the four compilation phases, the One Definition Rule, and the 2026 state of C++20 modules.

Overview

Which phase failed, and what did it see?

Every C and C++ build problem you will be asked to debug reduces to one question: which of the four phases failed, and what did that phase actually see? The gcc or g++ you invoke is not a compiler. It is a driver that inspects file suffixes and chains subprocesses together.

> [!IMPORTANT] The GCC manual is explicit about this four-stage model. Learn it, because each stage produces errors of a completely different character.

The translation unit

The compiler works on a translation unit: one source file plus every header the preprocessor pulled in, after #if and #ifdef were resolved. Each TU is compiled in isolation, with no view of the program as a whole.

  • Parallel builds are possible — TUs do not depend on each other at compile time
  • The One Definition Rule is necessary — No single TU can see a conflicting definition elsewhere
  • ODR violations are "ill-formed, no diagnostic required" — The compiler may say nothing and let you find out in production

Entities that may appear in many TUs

Inline functions, inline variables, class definitions and templates may be defined in multiple TUs. The condition: every definition is token-for-token identical and every name resolves the same way.

> [!NOTE] Templates cannot be compiled ahead of use: the compiler needs the concrete template arguments to instantiate a body. That single fact is why template definitions live in headers, why headers keep growing, and why C++ compiles slowly.

The include graph is the cost model

Build time in a large C++ project is a function of the include graph, not of source line count. If 900 TUs transitively include a header, changing one line in it costs 900 recompiles, each re-parsing and re-instantiating the same templates.

  • Forward declarations — Includes needed only for pointers or references — None
  • Pimpl idiom — Implementation dependencies, hidden behind an opaque pointer — One indirection
  • extern template plus explicit instantiation — Redundant template instantiation, compiled once — Closed set of instantiations
  • Precompiled headers — Re-parsing a stable prefix — Must match flags
  • Unity builds — Header parsing, amortized across many .cpp files — ODR risk, worse incrementality

C++20 modules in 2026

Modules replace textual inclusion with a compiled module interface that is parsed once and imported. The honest status: the compilers mostly work, the build systems mostly do not, and the standard library module is still the weak link.

  • GCC — Still gated behind -fmodules; support described as experimental
  • Clang — Documented two-phase model with .cppm interfaces and .pcm BMIs; BMIs are compiler-specific and not distributable

> [!WARNING] The scanning pass must run before any compilation can be ordered. It inverts the build system's dependency model, and that is the main reason adoption is slow.

What interviewers probe

Interviewers check whether you understand cause and effect, not whether you can recite flags. Good answers start by naming the phase the evidence points at.

  • Walk me through hello.cpp to ./hello — All four, plus runtime loading
  • Why is this header expensive? — Preprocess and compile, via the include graph
  • Why did the link fail when the compile succeeded? — Link
  • Why does the test binary behave differently from production? — Preprocess: different macros, so an ODR hazard

Key concepts

Translation unit
One source file after preprocessing: the file itself plus every transitively included header, with conditional compilation resolved. The compiler processes exactly one TU per invocation and produces exactly one object file. It has no visibility into any other TU, which is the root cause of every link-time and ODR surprise.
The four phases
Preprocess (-E, produces .i or .ii), compile proper (-S, produces .s via cc1 or cc1plus), assemble (-c, produces .o via as), link (produces the executable or shared object via collect2 driving ld). Each phase has a distinct failure signature; identifying the phase is the first step of any build diagnosis.
One Definition Rule (ODR)
At most one definition of any definable item per translation unit. Across TUs, inline functions, inline variables, class types, and templates may be defined more than once only if every definition is token-for-token identical and all names resolve identically. Violations are ill-formed, no diagnostic required — the compiler is allowed to stay silent and the linker will happily pick one definition arbitrarily.
Declaration versus definition
A declaration introduces a name and type (extern int x;, void f();, class Foo;). A definition provides storage or a body (int x;, void f() {}, class Foo { ... };). Headers carry declarations plus the ODR-exempt categories; source files carry definitions. Getting this boundary wrong is the most common source of both link errors and rebuild bloat.
Explicit instantiation and extern template
template class Foo; in one .cpp forces the compiler to emit the full instantiation there. extern template class Foo; in the header tells every other TU not to instantiate it. Together they move template codegen out of every TU into one, cutting both compile time and object size for heavily used instantiations.
Pimpl idiom
A class exposes only a pointer to an incomplete Impl type in its header; all real members and their includes move into the .cpp. Clients recompile only when the public interface changes, not when a private member or a private dependency changes. The cost is one heap allocation per object and one pointer indirection per member access.
Precompiled header (PCH)
A serialized snapshot of the compiler state after parsing a fixed set of headers, reused across TUs. GCC treats .h files as PCH candidates and looks for a .gch alongside the header. Effective only when the header set is genuinely stable and every TU uses identical flags — a flag mismatch silently invalidates the PCH and you pay the parse cost anyway.
C++20 module and BMI
A module interface unit (conventionally .cppm in Clang) is compiled into a built module interface, a .pcm file, which importers consume instead of re-parsing text. BMIs are compiler-version-specific, are not a distribution format, and are not an information-hiding mechanism — they contain everything used to build them in recoverable form. Build order requires a dependency scan (clang-scan-deps, P1689 format) before compilation can begin.

When to use it

  • Diagnosing why a one-line header change triggers a 30-minute rebuild — the include graph is the model you reason with
  • Deciding whether a new type belongs in a header, behind a forward declaration, or behind a pimpl boundary
  • Explaining an undefined reference for a template that visibly exists in the codebase
  • Auditing a build for ODR hazards after discovering that different targets compile the same headers with different -D flags
  • Evaluating precompiled headers, unity builds, or C++20 modules as a build-time intervention and being able to state the cost of each

Interview questions

  • Walk me through everything that happens between hello.cpp and a running ./hello.
  • What is the One Definition Rule, and how does it actually break in production?
  • Why do templates have to live in headers, and what do you do when that is unacceptable?
  • Our build takes 40 minutes and touching one header rebuilds 800 files. Walk me through fixing it.
  • Where do C++20 modules actually stand in 2026, and would you adopt them?

Sign up free to read the full topic

More DevOps topics

Start free on Cariara · Coding problems · System design studio