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:
Using
this->:this->reset(); // Dependent name; looked up in Phase 2Using explicit scope qualification:
Base<T>::reset(); // Dependent name; looked up in Phase 2 (disables virtual dispatch)Using a
usingdeclaration: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>:: |