Two-Phase Lookup in C++ Templates Explained

Two-phase lookup is the mechanism used by modern C++ compilers to parse and validate templates in two distinct stages: definition time and instantiation time. This article breaks down how two-phase lookup works, distinguishes between dependent and non-dependent names, explains why dependent base classes require explicit qualifiers like this->, and clarifies the role of the typename and template disambiguation keywords.


Understanding the Need for Two Phases

Before template instantiation, a C++ compiler does not know what concrete types will be passed as template arguments. However, it still needs to verify that the template contains valid C++ code. If compilation were deferred entirely until instantiation, syntax errors in unused templates would go unnoticed, and names might accidentally bind to unintended global symbols.

To solve this, the C++ standard separates name resolution in templates into two distinct phases.


Phase 1: Definition Time (Non-Dependent Names)

Phase 1 occurs when the compiler first reads the template definition, before any specific type arguments are provided.

During this phase:

  • Syntax checking: The compiler checks the general syntax of the template.
  • Non-dependent name resolution: Any identifier whose meaning does not depend on the template parameters is looked up immediately in the context of the template definition.

A non-dependent name is an identifier that does not rely on template parameters (such as built-in types like int, standard library functions, or local variables independent of T).

void logMessage() { /* ... */ }

template <typename T>
void process(T value) {
    logMessage(); // Non-dependent name: resolved in Phase 1
    nonExistentFunction(); // Error in Phase 1: fails even if process() is never instantiated
}

If a non-dependent name cannot be resolved or is used incorrectly, the compiler emits an error during Phase 1, regardless of whether the template is ever instantiated.


Phase 2: Instantiation Time (Dependent Names)

Phase 2 occurs when the template is actually instantiated with concrete types (e.g., process<int>(42)).

During this phase:

  • Dependent name resolution: Any identifier that depends on a template parameter is looked up.
  • Lookup context: The compiler searches names visible at the point of definition as well as names brought in via Argument-Dependent Lookup (ADL) at the point of instantiation.

A dependent name relies on a template parameter in some way, such as:

  • The type of a variable depending on T.
  • A function call passing an argument of type T.
  • A nested type or member accessed through T (e.g., T::value_type).
template <typename T>
void display(T obj) {
    print(obj); // Dependent name: resolved in Phase 2 via ADL based on T's actual type
}

The Dependent Base Class Problem

One of the most common issues arising from two-phase lookup involves class templates that inherit from a template-dependent base class:

template <typename T>
struct Base {
    void reset() {}
};

template <typename T>
struct Derived : Base<T> {
    void clear() {
        reset(); // Compilation error under standard C++
    }
};

In the example above, reset() does not depend on T syntactically, so the compiler treats it as a non-dependent name and attempts to resolve it during Phase 1. However, during Phase 1, the compiler does not inspect Base<T> because Base<T> could be specialized later for a specific type.

Because reset() is not found in the global or local scope during Phase 1, lookup fails.

The Solution

To tell the compiler to defer lookup to Phase 2, the name must be made explicitly dependent:

  1. Using this->:

    this->reset(); // Dependent name; looked up in Phase 2
  2. Using explicit scope qualification:

    Base<T>::reset(); // Dependent name; looked up in Phase 2 (disables virtual dispatch)
  3. Using a using declaration:

    using Base<T>::reset; // Introduces reset into Derived scope

Disambiguation with typename and template

Because the compiler does not know the exact definition of a dependent type during Phase 1, it assumes by default that dependent names represent variables, values, or methods, not types or template declarations.

The typename Disambiguator

If a dependent qualified name refers to a type, you must prefix it with typename:

template <typename T>
void allocate() {
    typename T::iterator it; // Tells compiler that 'iterator' is a type, not a static member variable
}

The template Disambiguator

If a dependent member is a member template, you must use the .template or ->template syntax so the compiler parses < as the start of template arguments rather than the "less-than" operator:

template <typename T>
void execute(T obj) {
    obj.template run<int>(); // Informs the compiler that 'run' is a member template
}

Summary of Rules

Feature Phase 1 (Definition Time) Phase 2 (Instantiation Time)
Target Names Non-dependent names Dependent names
Lookup Scope Scope at template definition Definition scope + ADL at instantiation point
Error Handling Syntax errors, missing non-dependent symbols Type mismatches, missing member functions in T
Dependent Base Members Not visible Visible via this-> or Base<T>::