Skip to content
C++
Language
Intermediate

Phases of Translation

The nine sequential phases the C++ standard mandates to transform raw source text into a linked executable.

Phases of Translationsince C++98

The C++ standard defines nine sequential phases that transform raw source bytes into a linked executable, where each phase's output feeds directly into the next, and conforming programs must behave as if these phases ran in strict order.

Overview

The standard does not mandate how a compiler is implemented β€” only that observable behaviour matches the nine-phase model. Real toolchains interleave phases for performance, but correctness is judged against the sequential model. Internalising the phase order resolves a class of otherwise mysterious bugs: why __LINE__ expands before a macro argument is evaluated, why string literal concatenation happens after preprocessing, why an undefined reference is a linker error rather than a compiler error, and why dependent names in templates resolve differently than non-dependent ones.

Phase 1 β€” Character Mapping

The source file's physical bytes are mapped to the translation character set. Characters outside the basic source character set are replaced by universal character names (\uNNNN / \UNNNNNNNN). Prior to C++23 the translation character set was implementation-defined; since C++23 it is Unicode, making encoding behaviour significantly more portable.

Practical consequence: a file saved as UTF-8 can contain non-ASCII identifiers and string content β€” the compiler normalises them in Phase 1 before anything else runs.

Phase 2 β€” Line Splicing

Every line whose last character before the newline is \ is joined with the following line; the backslash-newline pair is deleted. This occurs before tokenisation, meaning the splice can break or join tokens:

cpp
#define LONG_\
MACRO 42        // defines LONG_MACRO after splicing

const char* msg = "hel\
lo";             // valid: same as "hello"

A common trap: a backslash at the end of a // comment continues the comment to the next line, silently swallowing the following statement:

cpp
// do nothing here \
int x = 0;   // this line is inside the comment β€” x is never declared

Phase 3 β€” Tokenisation into Preprocessing Tokens

The spliced character stream is decomposed into preprocessing tokens and whitespace. Categories are: header names, identifiers, preprocessing numbers, character literals, string literals, operators/punctuators, and a catch-all for individual characters that fit no other category.

The governing rule is maximal munch: the lexer always forms the longest valid token. This is why a+++b parses as (a++) + b rather than a + (++b). It is also why, prior to C++11, vector<vector<int>> was a compilation error: >> was tokenised as the right-shift operator. C++11 introduced context-sensitive angle-bracket handling to fix this.

Preprocessing numbers are intentionally a superset of integer and floating-point literals β€” 1.5e+10 and even 0x1p+3 are valid preprocessing numbers in Phase 3. Types and values are assigned only in Phase 7.

Phase 4 β€” Preprocessing

The preprocessing token stream is executed: #include directives splice in other files (recursively restarting from Phase 1 for each included file), macro definitions are recorded, macro invocations are expanded, and #if/#ifdef/#elif/#else/#endif prune branches. #pragma, _Pragma, and #line are also consumed here.

cpp
#define MAX(a, b) ((a) > (b) ? (a) : (b))

// After Phase 4 the compiler sees no directives β€” only the expansion:
int x = MAX(foo(), bar());
// becomes: int x = ((foo()) > (bar()) ? (foo()) : (bar()));

The entire #include graph β€” often hundreds of headers β€” is flattened into a single preprocessing token sequence. This is why inclusion order can silently change behaviour and why headers lacking include guards cause redefinition errors.

Since C++20, import declarations bypass Phase 4 for module units. Preprocessor macros defined in one module unit do not cross module boundaries, which is one of the key hygiene guarantees modules provide.

Phase 5 β€” String Literal Encoding

The encoding of each character and string literal is resolved. Source-file characters within literals are converted to the execution character set. The full range of encoding prefixes is handled here:

cpp
const char*     a = "hello";      // execution narrow encoding (since C++98)
const wchar_t*  b = L"hello";     // execution wide encoding (since C++98)
const char16_t* c = u"hello";     // UTF-16 (since C++11)
const char32_t* d = U"hello";     // UTF-32 (since C++11)
const char8_t*  e = u8"hello";    // UTF-8, type char8_t (since C++20)

Phase 6 β€” String Literal Concatenation

Adjacent string literal tokens are concatenated into a single literal. This is the mechanism behind multi-line string idioms:

cpp
const char* sql =
    "SELECT id, name "
    "FROM users "
    "WHERE active = 1";   // three tokens merged into one here

Because Phase 6 follows Phase 4, macro expansion happens first. You cannot rely on a macro to produce one half of a concatenated pair β€” both halves must already be string literal tokens before Phase 6 runs.

Concatenating literals of differing encoding prefixes follows defined compatibility rules. Mixing a narrow literal with u8 is well-defined; mixing u with U is ill-formed.

Phase 7 β€” Compilation

The preprocessed, concatenated token sequence is parsed, type-checked, and translated into an object file (or equivalent). Semantic analysis, overload resolution, unqualified and qualified name lookup, template parsing (distinct from full instantiation), constant evaluation, and code generation all occur here. This phase produces the vast majority of diagnostics developers encounter.

cpp
// The extern reference is unresolved here β€” the linker handles it in Phase 9.
extern int external_counter;
int read_counter() { return external_counter; }

Since C++11, constexpr functions may be evaluated here for constant expressions. Since C++20, consteval functions must be fully evaluated here β€” any consteval call that cannot be resolved at compile time is a hard error in Phase 7.

Phase 8 β€” Template Instantiation

Template specialisations required by the translation unit are instantiated. Non-dependent names were resolved in Phase 7; dependent names are looked up here, at the point of instantiation. This is two-phase lookup, mandated since C++98 and a frequent source of portability bugs when code was only tested on compilers that deferred all lookup to instantiation time.

cpp
template <typename T>
void print(const T& v) {
    // std::cout β€” non-dependent, looked up in Phase 7
    // v.describe() β€” dependent, looked up in Phase 8 in T's associated namespaces
    std::cout << v.describe() << '\n';
}

SFINAE (since C++98, formalised further in C++11) and Concepts (since C++20) constrain which instantiations are attempted, preventing Phase 8 from firing on non-viable candidates.

Phase 9 β€” Linking

Translation units and library artefacts are combined. The linker resolves external references, merges sections, applies link-time optimisation (LTO) if enabled, and produces the final executable or shared object. Errors that survive to Phase 9 β€” undefined reference, multiple definition, ODR violations β€” indicate mismatched declarations across translation units or missing library dependencies.

cpp
// translation_a.cpp
void helper() { /* definition */ }

// translation_b.cpp
extern void helper();           // declaration β€” resolved in Phase 9
void caller() { helper(); }

Common Pitfalls

Macro ordering relative to #include. Phase 4 processes directives sequentially within each translation unit. A macro intended to guard or configure a header must be #defined before the #include, not after.

Line continuation inside comments. A \ at the end of a // comment (consumed in Phase 2) silently absorbs the next line. Compilers may warn about this with -Wcomment.

>> in nested templates before C++11. map<string, vector<int>> requires C++11. In C++03 the space before >> is mandatory: map<string, vector<int> >.

ODR violations across translation units. If two TUs define the same class or inline function with differing bodies, Phase 9 behaviour is undefined; the linker may pick one silently. C++20 modules prevent this for module-owned entities.

Two-phase lookup in derived templates. Accessing a base class member through an unqualified name inside a template fails Phase 7 lookup on conforming compilers because the base is dependent. Use this->member or Base<T>::member to make the dependency explicit.

Trigraphs and Phase 1 History

Before C++17, Phase 1 included a third sub-step: trigraph replacement. Trigraphs were three-character sequences beginning with ?? that the compiler substituted with a single character before any other processing took place. They existed because the ISO 646 character set used by certain European and national-standards keyboards did not include all characters in C's basic source character set.

The nine trigraphs and their substitutions:

TrigraphReplacementMeaning
??=#hash / pound
??/\backslash
??'^caret
??([left bracket
??)]right bracket
??!|vertical bar
??<{left brace
??>}right brace
??-~tilde

Because trigraph substitution happened in Phase 1 β€” before tokenisation, before comment processing, before string literal parsing β€” they could silently alter the meaning of code in ways that were hard to spot:

cpp
// C++03/C++14: what does this print?
printf("What??!\n");
// The ??! trigraph maps to |, so this printed: What|
// (newline was a separate escape sequence processed later)

// A comment ending in ??/ was actually a line continuation:
// This comment ends here ??/
int x = 0;  // silently part of the comment in C++03!

The trigraph for backslash (??/) was especially treacherous: it combined with Phase 2 line-splicing to make comment continuation invisible to the reader.

Why they were removed (C++17, paper N4086). By the time the C++17 committee deliberated, every keyboard in common use had direct access to all basic-source-character-set symbols. GCC dropped trigraph support by default in version 5.1; Clang disabled them by default in 3.5. The sequences were primarily a source of accidental bugs. Removing them simplified Phase 1 and eliminated an entire class of cryptic preprocessor surprises. Compilers that must support them for legacy codebases may offer a compatibility flag (GCC: -trigraphs, MSVC: /Zc:trigraphs).

Digraphs are different. The two-character digraph alternatives (<: for [, :> for ], <% for { , %> for }, %: for #) were deliberately retained. Unlike trigraphs, digraphs are processed in Phase 3 as alternative tokens β€” they appear as their own distinct token category and never silently mutate string literals or comments. The committee judged them safe to keep.

C++20 Modules and the Phase Model

C++20 named modules are the most significant change to the phase model since C++ was standardised. They affect how Phases 4, 7, 8, and 9 interact, and they introduce a new form of linkage that sits between the traditional internal and external kinds.

How Modules Change Phase 4

With header-based code, Phase 4 (#include) works by textually splicing included files into the current translation unit, recursively restarting Phases 1–4 for each file. The entire #include graph collapses into a single preprocessing token stream. Every macro defined anywhere in that graph is visible everywhere downstream.

Named modules replace textual inclusion with semantic importation:

cpp
// Traditional header-based (Phase 4 textual inclusion):
#include <vector>     // entire vector header token-pasted here
#define MY_MACRO 42   // leaks into all subsequent includes

// C++20 module-based (Phase 4 import declaration):
import std.vector;    // compiled binary interface β€” no token pasting
// MY_MACRO defined here does NOT cross the module boundary

The key Phase 4 guarantee modules provide: macros do not cross module boundaries. A #define inside a named module unit cannot affect code that imports it, and vice versa. This eliminates one of the largest categories of header ordering bugs.

A module unit's source file is itself processed through Phases 1–7 independently (producing a compiled module interface, or CMI), and importers receive only the exported declarations, not raw tokens.

The Global Module Fragment

Because most existing libraries are not yet module-aware, C++20 provides the global module fragment β€” a region before the module declaration where #include directives are still legal:

cpp
module;                   // begin global module fragment
#include <cstdio>         // attaches to the global module, not named module
#include "legacy_lib.h"   // same

export module my_module;  // named module begins here
// cstdio and legacy_lib are usable but their symbols
// are attached to the global module, not exported from my_module

The global module fragment is the only place inside a module-unit source file where #include is safe to use without polluting the named module's interface.

Module Linkage (Phase 9 Impact)

C++20 modules introduce a third kind of linkage alongside internal (TU-only) and external (cross-TU):

  • Internal linkage β€” entity is visible only within the defining translation unit (static, anonymous namespace).
  • External linkage β€” entity is "the same" across all translation units; the traditional cross-TU model.
  • Module linkage β€” entity is "the same" across all translation units that belong to the same named module, but is invisible to translation units outside the module.
cpp
export module geometry;

// External linkage β€” importers can use this:
export struct Point { double x, y; };

// Module linkage β€” shared across geometry's TUs, hidden from importers:
double origin_distance(Point p);

// Internal linkage β€” visible only in this TU:
static constexpr double kEpsilon = 1e-9;

An entity has module linkage when it is: declared in the purview of a named module, not exported, and not declared with static or inside an anonymous namespace. At Phase 9 (linking), the linker must distinguish module-linkage symbols from external-linkage symbols β€” implementations achieve this via additional name mangling.

ODR in a module world. The One Definition Rule still applies, but modules surface a new failure mode: two modules independently exporting a class with the same fully-qualified name create a program that is ill-formed, no diagnostic required. The linker cannot reliably detect this when the conflicting modules are not directly imported in the same TU.

Build-System Implications

Unlike #include, import introduces a compilation dependency: a module's interface must be compiled before any TU that imports it. Build systems (CMake 3.28+, Ninja 1.11+, MSBuild) must compute a dependency graph before scheduling compilation. This is fundamentally different from the parallel-compilation freedom that header-only code enjoys β€” though the reduction in repeated parsing generally makes the total wall-clock time shorter.

Two-Phase Name Lookup: Phases 7 and 8 in Depth

Two-phase name lookup is the mechanism by which the C++ standard reconciles two needs that would otherwise conflict: templates must be usable before their arguments are known (Phase 7), and template bodies must be able to refer to names that depend on the concrete type argument (Phase 8).

The Two Phases

Phase 1 of lookup (at template definition, during Phase 7 of translation). When the compiler first parses a template definition, it immediately resolves all non-dependent names β€” names that do not depend on any template parameter. This lookup happens in the context of the template definition's enclosing scope, using the declarations visible at that point.

Phase 2 of lookup (at instantiation, during Phase 8 of translation). When the compiler instantiates a template for a concrete argument T, it resolves all dependent names β€” names whose meaning changes based on T. This lookup uses both the context of the template definition (for regular lookup) and the associated namespaces and classes of the template argument (for argument-dependent lookup, ADL).

Dependent vs. Non-Dependent Names

A name is dependent if:

  • It is explicitly qualified through a type that depends on a template parameter (T::value, typename T::iterator).
  • It appears in a call whose argument types depend on T (enabling ADL on T's associated namespaces).
  • It is this->member inside a class template whose base class depends on T.

A name is non-dependent if none of the above conditions hold. Non-dependent names must be declared before the template definition β€” they will not be found by Phase 8.

cpp
template <typename T>
struct Derived : Base<T> {
    void g() {
        f();          // NON-dependent: looked up in Phase 7.
                      // Base<T>::f is NOT found β€” Base<T> is dependent.
                      // Error on conforming compilers.

        this->f();    // DEPENDENT: looked up in Phase 8.
                      // Base<T>::f is found at instantiation time. OK.

        Base<T>::f(); // Also dependent: qualified through Base<T>. OK.
    }
};

The rule that non-dependent names are not looked up in dependent base classes is the single most common source of "undeclared identifier" errors in derived class templates. The fix is always the same: make the name dependent by using this-> or a qualified name.

The typename and template Disambiguators

When a dependent name might refer to a type, the compiler cannot determine this in Phase 7 (because T is unknown). The standard requires the programmer to annotate such names:

cpp
template <typename T>
void process(T& container) {
    // Without typename: compiler assumes iterator is a value, not a type.
    // T::iterator it = container.begin();  // ERROR in Phase 7

    typename T::iterator it = container.begin();  // OK: explicit type hint

    // Similarly for member templates:
    // container.transform<int>();           // ERROR: < parsed as less-than
    container.template transform<int>();    // OK: explicit template hint
}

typename and template are not hints to the reader; they are Phase 7 grammar obligations. Omitting them is a hard error on conforming compilers even if every concrete instantiation would be valid.

Compiler History: MSVC's Late Adoption

MSVC historically deferred almost all name lookup to Phase 8 (instantiation), which meant non-conforming code compiled successfully on MSVC but failed on GCC and Clang. Microsoft introduced proper two-phase lookup support behind the /permissive- flag in Visual Studio 2017 15.3, and enabled it by default in later toolset versions. Code that relied on MSVC's permissive behaviour β€” particularly code that used undeclared names inside templates, expecting them to be found at instantiation β€” required fixes to compile under /permissive-.

The practical lesson: if a template compiles on MSVC but not on GCC/Clang, or produces different results between compilers, two-phase lookup is the first thing to investigate.

ADL at Instantiation Time

ADL (argument-dependent lookup) applies only to dependent unqualified function calls. This is what makes customisation points work:

cpp
template <typename T>
void swap_pair(T& a, T& b) {
    using std::swap;
    swap(a, b);  // dependent call β€” ADL fires in Phase 8
                 // finds user-defined swap(MyType&, MyType&) in MyType's namespace
                 // if no user swap, falls back to std::swap via the using declaration
}

The using std::swap provides a fallback visible at Phase 7 definition time, while ADL in Phase 8 allows a type-specific overload to win if one exists in the type's associated namespace. This two-phase interaction is the canonical C++11 swap idiom.

Concepts and Two-Phase Lookup (C++20)

C++20 Concepts constrain which instantiations Phase 8 attempts. A requires clause or concept constraint is checked as part of template argument deduction (before Phase 8 fires), which means constraint failures produce cleaner diagnostics than unconstrained template errors. However, two-phase lookup still applies inside constrained templates: non-dependent names are still resolved in Phase 7, and the same this-> and typename rules apply.

cpp
template <typename T>
    requires std::ranges::range<T>
void print_all(const T& r) {
    for (const auto& elem : r) {
        // Non-dependent call β€” must be declared at Phase 7:
        display(elem);          // ERROR if display not declared here
        ::display(elem);        // qualified β€” still non-dependent, still Phase 7
        detail::display(elem);  // same

        // To defer to Phase 8 via ADL, the call must be unqualified
        // and the argument type must be dependent:
        // (here elem's type depends on T::value_type, so ADL does fire)
    }
}

Macro Expansion Timing and Phase 4 Subtleties

Phase 4 is strictly sequential, not lazy. The preprocessor reads directives one token at a time from top to bottom; there is no lookahead, no deferred evaluation, and no way to inject a directive via macro expansion after the fact. This has several non-obvious consequences.

#include Inside a Macro Argument Does Not Work

A #include that appears as part of a macro argument is not a preprocessor directive β€” by the time the preprocessor scans macro arguments, it is inside a macro invocation context where # is the stringification operator, not a directive introducer. The following is ill-formed:

cpp
// C++11
#define WRAP(x) x

WRAP(
  #include <vector>   // NOT a directive here β€” preprocessor error
)

The correct approach is always to place #include directives at file scope, before the macro invocations that need the included content. This constraint is a direct consequence of Phase 4's single-pass, top-down execution model.

The Stringify Operator #

Inside a function-like macro, a # immediately before a parameter name converts the corresponding argument to a string literal before any further macro expansion of that argument. The stringification captures the argument's source text as written by the caller, including any internal whitespace, but excludes leading and trailing whitespace.

cpp
// C++11
#define STRINGIFY(x) #x
#define EXPAND_THEN_STRINGIFY(x) STRINGIFY(x)

#define VERSION 42

STRINGIFY(VERSION)              // β†’ "VERSION"  (no expansion first)
EXPAND_THEN_STRINGIFY(VERSION)  // β†’ "42"        (x expanded before stringification)

Indirection is required to expand before stringifying. Because # captures its operand before expansion, a two-layer macro (the outer expands first, the inner stringifies) is the standard idiom when you want the expanded value as a string. This is a Phase 4 timing rule, not a compiler quirk.

Token-Pasting with ##

The ## operator concatenates two preprocessing tokens into a single token. Both operands are macro-expanded before pasting only if they are not directly adjacent to ##; if a parameter is immediately preceded or followed by ##, it is not expanded first β€” only the substituted token text is pasted.

cpp
// C++11
#define CONCAT(a, b) a ## b
#define FOO_BAR 99

CONCAT(FOO, _BAR)   // β†’ FOO_BAR (the identifier, not 99)
                    // macro expansion does NOT occur before ##

Like stringify, this means the two-step indirection pattern is needed when you want expansion before pasting. Failing to understand this timing is a frequent source of macro debugging sessions, where the generated identifier looks right in theory but refers to an unexpected symbol.

Predefined Macros and Phase 4 Sequencing

Predefined macros such as __LINE__, __FILE__, and __DATE__ are substituted in Phase 4. Their values are fixed at the point each invocation is scanned, which means passing __LINE__ through multiple macro layers can still yield the line of the outermost call site β€” or not, depending on how the macro chain is constructed. Only __LINE__ at the outermost expansion point reliably records the call-site line number.

cpp
// C++11
#define LOG(msg) log_impl(__FILE__, __LINE__, msg)

// __LINE__ is expanded at the call site (Phase 4, sequentially),
// so it captures the line of the LOG(...) invocation, not log_impl's definition.
LOG("initialising subsystem");

Static Initialisation Order Across Translation Units

The static initialisation order fiasco is an artefact of Phases 8 and 9. Phase 8 instantiates templates and evaluates constexpr/constinit initialisers for a single translation unit; Phase 9 links multiple TUs together. The C++ standard guarantees that within a single TU, non-local variables with static storage duration are initialised in declaration order. Across TUs, however, the standard makes no guarantee about ordering β€” the linker is free to initialise TUs in any sequence.

The Classic Fiasco

cpp
// C++11
// file: counter.cpp
int initial_count = 100;   // TU-local static

// file: user.cpp
extern int initial_count;
int adjusted = initial_count + 1;  // undefined if user.cpp initialises first

If user.cpp's TU is initialised before counter.cpp's TU, initial_count is still zero-initialised (which happens before dynamic initialisation for all TUs), but its dynamic initialiser (which sets it to 100) has not yet run. adjusted captures the zero, not 100. The resulting value depends on link order β€” a form of undefined behaviour in practice, even though the individual TUs are well-formed.

constinit as a Phase 8 Guard (C++20)

C++20 introduced constinit to catch this class of bug at compile time. A constinit variable must have a constant initialiser β€” one that can be fully evaluated in Phase 7/8 without any dynamic runtime work. If the initialiser is not constant, the program is ill-formed.

cpp
// C++20
constinit int initial_count = 100;  // OK: integer literal is a constant initialiser

// constinit int bad = some_runtime_func();  // ERROR: not a constant initialiser

Because constinit values are computed by Phase 7/8 and stored directly in the object file, they are available before any dynamic initialisation runs. This makes them safe to reference from other TUs' static initialisers without ordering concerns.

The Construct-On-First-Use Idiom

Before constinit, the canonical workaround was to wrap the variable in a function that returns a reference to a local static:

cpp
// C++11
Counter& get_counter() {
    static Counter instance;   // initialised on first call, not at program start
    return instance;
}

Local statics are initialised the first time control passes through their declaration (guaranteed thread-safe since C++11). Any code that calls get_counter() before using the object ensures it is initialised, regardless of TU order. This defers Phase 9's initialisation-ordering problem to runtime, where it can be managed deterministically.

Zero-Initialisation Precedes Dynamic Initialisation

One subtlety that reduces the severity of the fiasco: before any dynamic initialisation runs, all variables with static storage duration are zero-initialised. This means that even if a TU's dynamic initialiser has not yet fired, the variable holds a predictable (if incorrect) zero value rather than indeterminate garbage. Zero-initialisation is a Phase 9 / loader-level operation, not a Phase 7/8 operation, and it is guaranteed to complete before any dynamic initialiser starts.

cpp
// C++11
int x;            // zero-initialised to 0 before dynamic init; then dynamically
int x = compute_x();  // set to compute_x() when this TU's dynamic init runs

See Also

  • reference/language/dependent-names
  • reference/language/constant-expressions
  • reference/language/adl
  • reference/language/attributes