C++ • GETTING STARTED AND TOOLING

Translation Units — Use header/source files and understand translation units (conceptual)

Understand how the C++ compiler transforms source and header files into independent translation units before linking.

Historical Context & Motivation

The concept of separate compilation — the ability to compile individual source files independently and later combine them into a single executable — is one of the most consequential design decisions in the history of systems programming. Before separate compilation became standard practice, programmers were forced to submit entire programs as monolithic text files, meaning that a single-character typo required recompiling every line of code. As programs grew from dozens of lines to hundreds of thousands, this approach became untenable. The translation unit model, inherited from C and refined by C++, solved this problem by defining a clear boundary around what the compiler processes in a single invocation.

1972
C Introduces the Preprocessor
Dennis Ritchie's early C compiler at Bell Labs introduced #include directives and the textual preprocessor, establishing the foundation for header files that could be shared across source files.
1978
K&R C Codifies Separate Compilation
The publication of "The C Programming Language" by Kernighan and Ritchie formalized the convention of splitting declarations into header (.h) files and definitions into source (.c) files, making separate compilation a mainstream practice.
1985
C++ 1.0 (Cfront) Extends the Model
Bjarne Stroustrup's Cfront translator added classes, inline functions, and templates on top of C's translation unit model, introducing new complexities like the One Definition Rule (ODR) to ensure correctness across translation units.
1998
ISO C++98 Formalizes Translation Phases
The first ISO C++ standard (ISO/IEC 14882:1998) defined nine translation phases, giving a rigorous specification to the process by which source code becomes object code.
2020
C++20 Introduces Modules
C++20 introduced modules as a modern alternative to header files, aiming to reduce the overhead and fragility of textual inclusion while preserving the translation unit concept.

The central question this lesson addresses is: when you invoke the compiler on a .cpp file, what exactly does it see, and how do header files, preprocessor directives, and the linker collaborate to produce a working program from many independently compiled pieces?

Core Principles & Definitions

Understanding translation units requires internalizing several foundational concepts that govern how C++ source code is organized, processed, and ultimately linked. These principles are not merely academic — they directly influence how you structure projects, debug linker errors, and reason about program correctness.

1

Translation Unit (TU)

A translation unit is the result of preprocessing a single source file: the source file's own code plus every header and file recursively #include'd into it. The compiler sees exactly one TU per invocation and produces one object file from it.
2

Declaration vs. Definition

A declaration introduces a name and its type to the compiler (e.g., void foo();). A definition provides the full implementation or storage. Declarations belong in headers; definitions belong in source files.
3

One Definition Rule (ODR)

Across all translation units in a program, each non-inline function or variable must have exactly one definition. Violating the ODR leads to linker errors or, worse, undefined behavior.
4

Linkage

A name with external linkage is visible across translation units. A name with internal linkage (declared static or in an anonymous namespace) is private to its TU. Linkage determines what the linker can and cannot resolve.
5

Include Guards / #pragma once

Because #include is textual substitution, the same header can be included multiple times in one TU. Include guards (#ifndef / #define / #endif) or #pragma once prevent duplicate definitions within a single TU.
✦ KEY TAKEAWAY
Think of a translation unit like a single chapter manuscript sent to an editor. The #include directives are photocopied appendices stapled to the front of that chapter — they become physically part of the manuscript the editor reads. The editor (compiler) has no idea that other chapters exist; only the publisher (linker) assembles all chapters into the final book. If two chapters both define the same glossary entry differently, the publisher catches the conflict — that's the One Definition Rule.

Visual Explanation — Anatomy of a Translation Unit

The diagram traces two source files (math.cpp and main.cpp) through the build pipeline. The preprocessor expands #include "math.h" by pasting the header's contents into each source file, forming two independent translation units. Each TU is compiled into an object file (.o), and the linker resolves cross-TU references — such as main.cpp calling add() defined in math.cpp — to produce the final executable.

Notice that the header file math.h does not produce its own translation unit. Headers are never compiled directly — they exist only to be textually included into .cpp files. This is why the number of translation units in a project equals the number of source files, not the number of header files. Each TU is an island: the compiler processes it without any knowledge of other TUs, relying entirely on declarations (from headers) to trust that symbols defined elsewhere will exist at link time.

The Nine Translation Phases

The C++ standard (§5.2 in C++23, formerly §2.2) defines nine translation phases that describe the conceptual steps a conforming compiler takes to transform source code into a linked program. While real compilers may interleave or merge these phases for efficiency, the standard guarantees that the observable result is as-if the phases executed in order. Understanding these phases clarifies where headers fit, when macros expand, and why certain errors occur at specific stages.

The nine translation phases defined by the C++ standard
PhaseNameWhat Happens
1Character MappingPhysical source file characters are mapped to the basic source character set. Trigraphs (deprecated in C++17, removed in C++23) are replaced.
2Line SplicingBackslash-newline sequences are removed, joining physical lines into logical lines.
3TokenizationSource is decomposed into preprocessing tokens and whitespace. Comments are replaced by single spaces.
4PreprocessingDirectives are executed: #include (recursively applying phases 1–4 to the included file), #define expansions, conditional compilation. This phase produces the translation unit.
5Character ConversionSource character set members in character/string literals are converted to the execution character set.
6String ConcatenationAdjacent string literal tokens are concatenated into single tokens.
7CompilationPreprocessing tokens become tokens. Syntactic and semantic analysis occurs. Templates are instantiated. The TU is translated into an object file.
8Template InstantiationInstantiation units are examined; required template specializations are produced. (Conceptually, the compiler may defer instantiation to this point.)
9LinkingAll translation units and instantiation units are combined. External references are resolved. The final executable or library is produced.
🔍 Phase 4 Is Where Headers Become Part of the TU
When the preprocessor encounters #include "math.h", it recursively applies phases 1 through 4 to the contents of math.h, then replaces the directive with the resulting tokens. After phase 4 completes, no #include directives remain — the translation unit is a flat sequence of tokens.

Header vs. Source File Conventions

Although the C++ standard does not technically mandate a separation between header and source files — any file can be #include'd — decades of practice have converged on a clear convention. Header files (typically .h, .hpp, or .hxx) contain declarations and certain definitions that must be visible in multiple TUs. Source files (typically .cpp, .cc, or .cxx) contain definitions that should exist in exactly one TU.

This side-by-side reference summarizes the items that belong in headers versus source files. Note that template definitions must go in headers because the compiler needs the full definition to instantiate templates in each TU that uses them. Similarly, inline and constexpr functions are exempt from the ODR's one-definition-per-program rule (they must still have identical definitions across all TUs).

The rationale behind these conventions is grounded in the ODR. When a header is included in multiple TUs, everything in that header gets duplicated into each TU. If the header contains a non-inline function definition, the linker will see multiple identical definitions and reject the program (on most toolchains, you'll see a multiple definition of 'foo' error). By placing only declarations in headers and definitions in source files, each symbol is defined in exactly one TU, satisfying the ODR.

Worked Example — Building a Multi-File Project

Let's walk through a small but complete multi-file C++ project that demonstrates the translation unit concept end-to-end. We have a simple geometry library with a Circle class.

Multi-File Project: Circle Area Calculator
1
Step 1 — Create the Header File (circle.h)The header declares the Circle class and its member functions. We protect against multiple inclusion with #pragma once. The header contains: #pragma once class Circle { public: Circle(double r); double area() const; private: double radius_; }; — notice that only declarations appear; no function bodies are provided here.
Header provides the interface — declarations only.
2
Step 2 — Create the Source File (circle.cpp)The source file includes its own header and defines the member functions: #include "circle.h" #include <cmath> Circle::Circle(double r) : radius_(r) {} double Circle::area() const { return M_PI * radius_ * radius_; } — after preprocessing, this file's translation unit contains the expanded contents of circle.h, the expanded contents of <cmath>, and the two function definitions.
Translation Unit 1 = circle.h + <cmath> + circle.cpp definitions.
3
Step 3 — Create main.cppThe main source file includes the Circle header so the compiler knows the class interface, then uses it: #include "circle.h" #include <iostream> int main() { Circle c(5.0); std::cout << "Area: " << c.area() << '\n'; return 0; } — this file's translation unit contains the expanded circle.h and <iostream>, plus the main() definition. Crucially, the compiler can type-check the call to c.area() because the declaration is visible, even though the definition is in another TU.
Translation Unit 2 = circle.h + <iostream> + main() definition.
4
Step 4 — Compile Each TU IndependentlyWe compile each source file into an object file: g++ -c circle.cpp -o circle.o and g++ -c main.cpp -o main.o. The -c flag tells the compiler to stop after producing object code — no linking. Each invocation processes exactly one translation unit.
Two object files: circle.o (contains add, area machine code) and main.o (contains main machine code with unresolved references).
5
Step 5 — Link the Object FilesThe linker combines both object files: g++ circle.o main.o -o circle_app. The linker resolves main.o's reference to Circle::Circle(double) and Circle::area() by finding their definitions in circle.o. If we had forgotten to compile circle.cpp, the linker would report an unresolved external symbol error.
Final executable: circle_app — prints "Area: 78.5398"

Strengths & Limitations of the Header/Source Model

Trade-offs of the header/source translation unit model
AspectStrengthLimitation
Build SpeedChanging one .cpp file requires recompiling only that TU, then re-linking. Incremental builds can be orders of magnitude faster than full rebuilds.Changing a widely-included header forces recompilation of every TU that includes it, potentially triggering cascading rebuilds across the entire project.
EncapsulationImplementation details in .cpp files are invisible to other TUs, providing a form of information hiding at the file level.Headers must expose class private members (for sizing), leaking implementation details. The PIMPL idiom or abstract interfaces are needed to fully hide them.
CorrectnessThe ODR gives a clear, enforceable rule: one definition per program. Linkers catch most violations immediately.ODR violations involving inline functions or templates with differing definitions across TUs cause undefined behavior that linkers often fail to detect.
ScalabilityLarge projects (millions of lines) can be organized into libraries with well-defined header interfaces, supporting team-level parallel development.Deeply nested #include chains can result in a single TU containing hundreds of thousands of lines after preprocessing, slowing compilation and increasing memory use.
ToolingBuild systems (Make, CMake, Ninja) track dependencies between source and header files, automating incremental rebuilds.Dependency tracking is fragile: implicit dependencies through #include are hard to manage correctly, leading to stale object files or missing rebuilds.
✦ KEY TAKEAWAY
The header/source model is like a legal contract system. Headers are the contracts (interfaces): every party (TU) reads the same contract and trusts that someone else (another TU) will fulfill the obligations (provide definitions). The linker is the court that enforces these contracts at link time. This system works remarkably well for decades-old codebases, but its textual inclusion mechanism carries real costs — C++20 modules aim to modernize this contract system with a binary, non-textual interface.

Connection to C++20 Modules and Advanced Concepts

The traditional header/source model has served C++ well for decades, but its limitations have motivated a fundamental evolution: C++20 modules. Modules replace textual inclusion with a semantic import mechanism, where the compiler processes a module interface once and produces a binary module interface (BMI) that importing TUs can consume without re-parsing. This eliminates the redundant parsing that occurs when hundreds of source files all #include the same headers.

Headers vs. C++20 Modules
FeatureHeaders (#include)Modules (import)
MechanismTextual copy-paste by preprocessorSemantic import from precompiled binary interface
Macro LeakageMacros from headers leak into the including TU and vice versaMacros do not cross module boundaries
Include OrderOrder of #include directives can change semanticsImport order is irrelevant
Build SpeedHeaders re-parsed in every TU that includes themModule interface compiled once; imported many times
ODR SafetySilent ODR violations possible with differing include contextsStronger ODR guarantees via single module interface
AdoptionUniversal — every C++ codebase uses headersEmerging — compiler and build system support still maturing (as of 2024)

Even with modules, the concept of the translation unit persists. A module interface unit and a module implementation unit are each their own translation units. The key difference is how they share information: instead of textual pasting, they share pre-digested declarations through the module system. Understanding translation units remains essential because modules are built on top of, not as a replacement for, the translation unit model. Additionally, related concepts such as link-time optimization (LTO), precompiled headers (PCH), and unity builds — where multiple .cpp files are concatenated into a single massive TU for faster compilation — all presuppose a solid understanding of what a translation unit is.

Practice Problems

PROBLEM 1 — CONCEPTUAL
A project has 3 source files (a.cpp, b.cpp, c.cpp) and 5 header files (x.h through z.h plus two others). How many translation units does this project produce, and why?
PROBLEM 2 — BASIC CALCULATION
Consider the following header file util.h (without include guards): int helper(int x); const int MAX = 100; And source file main.cpp: #include "util.h" #include "util.h" int main() { return helper(MAX); } Will this compile successfully? If not, what error occurs and how would you fix it?
PROBLEM 3 — INTERMEDIATE
A developer places the following function definition in a header file ops.h (with proper include guards): int square(int x) { return x * x; } This header is included by both a.cpp and b.cpp. Each file compiles successfully on its own. However, the linker reports an error when combining a.o and b.o. Explain why and provide two distinct fixes.
PROBLEM 4 — APPLIED
You are working on a large project with 200 source files. A commonly-included header config.h is modified to add a new constant. Your build system reports that 180 out of 200 source files need recompilation. Explain why this happens in terms of translation units, and propose a structural change to reduce the blast radius of header modifications.
PROBLEM 5 — CRITICAL THINKING
In a unity build, all .cpp files are concatenated (via #include) into a single unity.cpp, producing one giant translation unit. Analyze the implications of this approach: under what circumstances does it improve build times? What new categories of bugs can it introduce that would not exist in a traditional per-file TU model? How does it interact with static functions and anonymous namespaces?

Summary — Translation Units in C++

A translation unit is the fundamental unit of compilation in C++: a single source file (.cpp) combined with all the header files recursively included via #include directives. The preprocessor performs textual substitution to produce the TU, the compiler transforms each TU into an object file independently, and the linker resolves cross-TU references to produce the final executable. Headers contain declarations; source files contain definitions.

The One Definition Rule (ODR) requires that non-inline, non-template entities be defined in exactly one TU across the entire program. Include guards prevent duplicate definitions within a single TU, while the header/source separation convention prevents duplicate definitions across TUs. Linkage (external vs. internal) determines which names are visible across TU boundaries. Looking ahead, C++20 modules modernize the inclusion model with semantic imports, but the translation unit concept remains the bedrock of C++ compilation.

Varsity Tutors • C++ • Translation Units — Use header/source files and understand translation units (conceptual)