Skip to content
C++
Language
since C++98
Advanced

Two-Phase Name Lookup

"C++ two-phase lookup: non-dependent names bind at definition, dependent names at instantiation — typename, template keyword, ADL, and base class pitfalls."

Two-Phase Name Lookupsince C++98

Template name lookup occurs in two phases: non-dependent names are resolved at the point of template definition, while dependent names — those whose meaning changes with the template parameters — are resolved at the point of instantiation.

Overview

When the compiler sees a template definition, it cannot fully resolve names that depend on template parameters — it doesn't yet know what types those parameters will be. The standard therefore splits lookup into two phases with fundamentally different rules.

Phase 1 — at template definition: Non-dependent names must be resolvable now. A call to undefined_fn() with no template-dependent arguments is a phase 1 error even if the template is never instantiated.

Phase 2 — at point of instantiation (POI): Once the template is stamped with concrete types, dependent names are resolved. The POI is typically just after the enclosing namespace scope that triggered the instantiation. Unqualified dependent calls trigger ADL here, using the namespaces associated with the actual argument types.

cpp
template<typename T>
struct Foo {
    void f() {
        // Phase 1 — resolved at definition
        int x = 0;           // non-dependent — fine
        // undefined_fn();   // phase 1 hard error even if Foo<T> is never used

        // Phase 2 — resolved at instantiation
        T::value;            // qualified with T — dependent
        this->bar();         // 'this' is T-dependent
        sizeof(T);           // involves template parameter
    }
};

MSVC conformance note: Historically, MSVC's default mode skipped most of phase 1 and deferred all lookup to instantiation, silently accepting non-portable code. Since Visual Studio 2017, /permissive- enables conforming two-phase lookup. Always compile with /permissive- on MSVC and -pedantic on GCC/Clang to surface phase 1 errors that permissive mode would hide.


typename — Disambiguating Dependent Types

When a name is both dependent and qualified (T::something), the compiler defaults to treating it as a value (a static data member or enumerator). The typename keyword overrides this and declares that the name is a type.

cpp
template<typename Container>
void f(Container& c) {
    Container::iterator it;                      // error: compiler assumes 'iterator' is a value
    typename Container::iterator it2 = c.begin(); // OK

    typename Container::value_type v{};
    typename Container::size_type  n = c.size();
}

The rule applies wherever a dependent qualified name could be either a type or a value: alias declarations, return types, parameter types, and member declarations.

cpp
template<typename T>
struct Wrapper {
    using value_type = typename T::value_type;         // alias declaration
    typename T::reference front() { return data_[0]; } // return type
    void push(typename T::const_reference val);        // parameter type
private:
    T data_;
};

C++20 relaxation: Several contexts that unambiguously require a type — including elaborated type specifiers inside template argument lists and some positions in requires clauses — no longer require an explicit typename. This is a targeted syntactic relief, not a blanket removal. Both GCC 10+ and Clang 16+ implement it.


template — Disambiguating Dependent Template Members

When a dependent name is followed by <, the parser defaults to reading < as the less-than operator. The template keyword before the name signals that it is a template and < opens its argument list.

cpp
template<typename T>
void g(T& obj) {
    obj.cast<int>();            // parsed as (obj.cast < int) > ()  — error
    obj.template cast<int>();   // OK

    T::template make<double>(); // static template member
}

This appears most often in nested type spellings. The canonical example is allocator rebind:

cpp
template<typename Alloc>
class Container {
    // typename → it's a type; template → rebind<> is a template member
    using ByteAlloc = typename Alloc::template rebind<std::byte>::other;

    // Chained: nested template type inside a dependent template
    using NestedIter =
        typename std::allocator_traits<Alloc>::template rebind_alloc<int>;
};

The rule: whenever x.name<...> or X::name<...> has a dependent x/X, prepend template before name.


ADL in Templates

Unqualified dependent calls perform argument-dependent lookup at phase 2 using the namespaces associated with the concrete argument types. This is the mechanism behind every customizable algorithm in the standard library.

cpp
namespace geo {
    struct Point { double x, y; };
    void swap(Point& a, Point& b) noexcept;  // custom swap
    double distance(Point a, Point b);
}

template<typename T>
void sort_pair(T& a, T& b) {
    using std::swap;   // fallback if no ADL candidate exists
    swap(a, b);        // phase 2: ADL finds geo::swap when T = geo::Point
}

template<typename T>
double measure(T a, T b) {
    return distance(a, b);  // phase 2: ADL finds geo::distance
}

To suppress ADL deliberately — wrap the name in parentheses or qualify it:

cpp
template<typename T>
void explicit_std(T& a, T& b) {
    (swap)(a, b);       // parens suppress ADL — only std::swap considered
    std::swap(a, b);    // qualified call also suppresses ADL
}

Writing std::swap(a, b) directly is a common mistake: it suppresses the type's custom swap and falls back to the generic move-based one, which may be slower or incorrect for types with invariants.


this-> for Members of Dependent Bases

Names in a dependent base class are invisible to unqualified lookup in phase 1 — the compiler cannot examine Base<T> until T is known and doesn't speculate.

cpp
template<typename T>
struct Base {
    void helper() { /* ... */ }
    int value = 0;
    virtual void hook() {}
};

template<typename T>
struct Derived : Base<T> {
    void f() {
        helper();          // error: Base<T> not searched in phase 1
        this->helper();    // OK: makes the call dependent — deferred to phase 2
        Base<T>::helper(); // OK: explicit qualification — but suppresses virtual dispatch

        this->value;       // OK
        this->hook();      // OK, virtual dispatch preserved
    }
};

Prefer this->member over Base<T>::member for inherited members unless you specifically want to bypass virtual dispatch. Both satisfy the dependent lookup requirement, but only this-> preserves polymorphism.


Best Practices

  • Write typename before every dependent qualified name that denotes a type — don't rely on compiler extensions or inference.
  • Write .template / ::template whenever a dependent name is immediately followed by < for template arguments.
  • Use this->member (not Base<T>::member) to access inherited members in a template derived class unless suppressing virtual dispatch is intentional.
  • Always pair unqualified customization-point calls with a using declaration (using std::swap;) immediately before the call, not at file scope.
  • Compile with /permissive- (MSVC) or -pedantic (GCC/Clang) to catch non-dependent name errors that permissive two-phase implementations would silently defer.

Common Pitfalls

Specialization declared after instantiation. A specialization must be visible before the point at which the template is instantiated with that type, or the primary template is silently used:

cpp
template<typename T> void process(T);

template<typename T>
void dispatch(T t) { process(t); }

void use() {
    dispatch(42);  // instantiates dispatch<int>, which binds process<int>
                   // from the primary — the specialization below is too late
}

template<> void process<int>(int);  // must appear BEFORE dispatch<int> is instantiated

typename on non-dependent qualified names is either a no-op or an error depending on context. Don't scatter it defensively where names are not dependent.

Missing template in complex nested spellings:

cpp
// Wrong — '<' parsed as less-than:
typename Alloc::rebind<int>::other x;

// Right:
typename Alloc::template rebind<int>::other x;

Fully qualified calls disable ADL silently. Writing ::process(obj) or ns::process(obj) instead of the unqualified process(obj) means no ADL fires. The call compiles but ignores any type-specific overload in the argument's associated namespace.


Phase 1 in Depth: What "Definition Time" Really Means

Phase 1 is bounded strictly by what is declared in the enclosing scope at the moment the compiler parses the template body. The compiler performs a genuine name lookup pass — it is not merely a syntax check. Any unqualified, non-dependent name that is not declared by that point is a hard error, regardless of whether the template is ever instantiated and regardless of whether the name will exist later in the same translation unit.

cpp
void audit(int);  // declared before the template

template<typename T>
void process(T val) {
    audit(42);       // phase 1 OK — audit is non-dependent and declared above
    report(val);     // phase 2 — val is T-dependent, deferred to instantiation
    // log_event();  // would be a phase 1 error even if log_event() is
                     // defined later in this same file
}

void log_event();   // too late: the template body is already parsed

This "sealed at definition" property has a critical implication: you cannot retroactively inject names into phase 1 lookup by adding declarations later, even in the same translation unit, even in the same namespace. The compiler's view of the enclosing scope is frozen at the point where the template definition closes.

The same rule applies to function overloads. If you add an overload of a non-dependent function after the template definition, the template will never see it:

cpp
void notify(long);   // overload A — visible at template definition

template<typename T>
void alert() {
    notify(0);  // phase 1 selects notify(long) — the only candidate visible now
}

void notify(int);   // overload B — added after template definition
                    // alert<X>() will still call notify(long), never notify(int)

This contrasts sharply with ordinary function calls outside templates, where all overloads declared anywhere before the call site are considered. The asymmetry is intentional: it lets the compiler validate template bodies independently of the types they will eventually be used with.


Phase 2 in Depth: The ADL Asymmetry

When a dependent call is resolved in phase 2, two distinct lookup strategies run, and they search different territories:

Ordinary unqualified lookup at phase 2 searches only the definition context — the same scope that was available during phase 1. It does not gain access to anything that was added after the template was defined. Its search space is identical to what phase 1 would see for that name, minus the dependent-argument check.

Argument-dependent lookup (ADL) at phase 2 searches both the definition context and the instantiation context — the namespace scope surrounding the point where the template is first stamped with concrete types. This asymmetry is deliberate: ADL is the designated extension mechanism that lets user-defined types in their own namespaces participate in generic algorithms without modifying the algorithm's namespace.

cpp
namespace geo {
    struct Vec2 { double x, y; };
    double dot(Vec2 a, Vec2 b);       // defined in geo namespace
}

namespace math {
    template<typename T>
    double magnitude_sq(T v) {
        return dot(v, v);  // dependent call — phase 2
        // Ordinary lookup: searches math:: (definition context) — no dot()
        // ADL: searches geo:: (instantiation context) — finds geo::dot
        // Result: geo::dot is called
    }
}

// At the call site:
double d = math::magnitude_sq(geo::Vec2{3.0, 4.0});  // ADL finds geo::dot

The ADL-only extension of search space is the reason that customization-point idioms work. std::swap, std::begin, and range-for's begin/end all rely on ADL finding user-provided overloads in the type's associated namespace at instantiation time, not at the algorithm's definition time.

The flip side is that ADL can also find unintended candidates introduced in the argument's namespace. If geo later adds a free dot that takes different arguments but is a better match after implicit conversion, it can silently hijack existing template instantiations — a phenomenon sometimes called "ADL poisoning." This is why namespace hygiene and explicit qualification matter in large codebases.

The C++ standard (currently [temp.dep.candidate]) formalizes the asymmetry: if a better match for a dependent call would have been found had ADL searched all translation units exhaustively, the program is ill-formed (no diagnostic required). This is the "infinite TU" rule — you cannot write a program whose correctness depends on ADL not finding an overload that exists somewhere in the codebase.


Dependent Base Classes: The this-> Workaround in Full

The existing this-> section above shows the pattern. This section explains why it is necessary at a deeper level and covers the third workaround — using declarations — which the summary omits.

When the compiler parses Derived<T>, it knows that Base<T> is a base class, but it cannot examine Base<T>'s members until T is known. The standard explicitly prohibits the compiler from "peeking" into a dependent base: doing so would require instantiating the primary template (or finding a partial/explicit specialization), which is not permissible during phase 1. So Base<T> is simply excluded from unqualified lookup in the derived class body.

The result is a surprisingly sharp error — the compiler does not warn that it skipped the base, it reports the name as undeclared:

cpp
template<typename T>
struct Base {
    void configure(int n);
    using size_type = std::size_t;
    static constexpr int kLimit = 256;
};

template<typename T>
struct Derived : Base<T> {
    void setup(int n) {
        configure(n);          // error C3861 / error: 'configure' not declared
        int x = kLimit;        // error: 'kLimit' not declared
        size_type sz = 0;      // error: 'size_type' not declared (even as a type)
    }
};

Three fixes exist, each with different trade-offs:

cpp
template<typename T>
struct Derived : Base<T> {
    // Fix 1: this-> — makes the expression dependent, deferred to phase 2
    //        virtual dispatch is preserved; works for both data and functions
    void setup_v1(int n) {
        this->configure(n);
        int x = this->kLimit;
    }

    // Fix 2: explicit Base<T>:: qualification — also dependent, phase 2
    //        but virtual dispatch is SUPPRESSED (like a qualified call)
    void setup_v2(int n) {
        Base<T>::configure(n);   // calls Base<T>::configure directly
        int x = Base<T>::kLimit;
    }

    // Fix 3: using-declaration — brings the name into Derived's scope
    //        resolves at phase 1 once Derived is fully known; virtual dispatch
    //        is preserved; most readable for names used repeatedly
    using Base<T>::configure;
    using typename Base<T>::size_type;

    void setup_v3(int n) {
        configure(n);    // now found via the using-declaration
        size_type sz = 0;
    }
};

The using typename Base<T>::size_type form requires typename because size_type is a dependent type name. Omitting typename there is the same error as omitting it from typename Container::iterator in any other dependent context.

MSVC in legacy permissive mode (/permissive without the dash, or older VS versions) silently compiles the first erroneous Derived above, because it defers all lookups including phase 1 ones. GCC and Clang have enforced the standard behavior since their early template implementations. This is one of the most common sources of "compiles on MSVC, fails on GCC" portability bugs.


The template Disambiguator: When and Why It Fails Silently

The template keyword before a member name in a dependent context does more than signal to the programmer — it tells the parser to switch from expression-parse mode to template-argument-list mode when it encounters <. Without it, the parser commits to reading < as the less-than operator before it has enough information to backtrack.

The most confusing aspect is that the failure is not always a clean compile error. On some compilers and in some expressions, the parser may silently produce a valid (but wrong) parse tree where obj.method<T>() is read as (obj.method < T) > () — comparing obj.method to T, then comparing the result to (). This may even compile if the types support operator<.

cpp
template<typename Factory>
void create_all(Factory& f) {
    // Wrong — parsed as comparison, not a template call:
    // auto p = f.make<Widget>();          // (f.make < Widget) > ()

    // Correct — template keyword prevents the misparsing:
    auto p = f.template make<Widget>();

    // Also applies to pointer-to-member and arrow:
    Factory* fp = &f;
    auto q = fp->template make<Widget>();

    // And to static template members via ::
    Factory::template create<int>();
}

The keyword is required whenever all three conditions hold simultaneously: the object or type being accessed is dependent, the member name names a template, and the name is immediately followed by < for template arguments. Drop any one condition and template is unnecessary (though harmless before the name in most positions).

A nested chain compounds the requirement:

cpp
template<typename Alloc>
struct Pool {
    // Each :: or . on a dependent object needs its own template keyword
    // if the member to its right is a template followed by <
    using NodeAlloc =
        typename std::allocator_traits<Alloc>::template rebind_alloc<Node>;

    // typename → the overall expression is a type
    // template → rebind_alloc is a template member of a dependent type
    // without template: parser reads < as less-than and chokes on Node>
};

MSVC Conformance Mode: /permissive- and Two-Phase Lookup

MSVC's historical implementation stored function template bodies as raw token streams and deferred nearly all parsing — including phase 1 lookup — until a template was instantiated. This meant that a great deal of non-portable template code compiled silently on MSVC while being rejected by GCC and Clang.

Starting with Visual Studio 2017 (version 15.3), the compiler option /permissive- begins enforcing two-phase lookup. It was extended further in VS 2017 15.7. As of Visual Studio 2019 16.11, /permissive- is implied by /std:c++20, and it is required for C++20 Modules support.

What /permissive- changes for templates:

  • The compiler now parses function template bodies and class template bodies completely at definition time, not just at instantiation.
  • Non-dependent names that are undeclared at definition time produce C3861 ("identifier not found") even if the template is later instantiated in a context where the name exists.
  • The typename keyword is now required before dependent qualified type names; its absence is an error rather than a warning.
  • The template keyword is now required before dependent template member names followed by <.
  • Unqualified calls to names in dependent base classes are rejected; this-> or explicit qualification is required.

The escape hatch: /Zc:twoPhase- disables two-phase lookup specifically while keeping all other conformance improvements that /permissive- enables. This is useful during incremental migration of legacy codebases — you can turn on strict conformance for everything except the two-phase behavior while you update template code.

cmake
# CMake — enable conformance mode
target_compile_options(mylib PRIVATE $<$<CXX_COMPILER_ID:MSVC>:/permissive->)

# To disable just two-phase lookup while keeping everything else:
target_compile_options(mylib PRIVATE
    $<$<CXX_COMPILER_ID:MSVC>:/permissive- /Zc:twoPhase->)

Portability strategy: Code that compiles only under MSVC legacy mode almost certainly has phase 1 name-resolution bugs. The reliable approach is to enable /permissive- (or at minimum /Zc:twoPhase) on MSVC and -pedantic -Werror on GCC/Clang in CI, so that both compiler families validate the same standard rules. Portability bugs of this kind are far cheaper to fix when the CI pipeline catches them immediately than when they surface on a new platform months later.


C++20 Modules and Two-Phase Lookup

C++20 modules do not change the fundamental two-phase rule, but they change what names are reachable at instantiation time — and therefore what ADL can find in phase 2.

In the header-file model, #include is textual: every declaration in a header is unconditionally visible everywhere the header is included. This means that any declaration reachable at the template's definition site is also reachable at any instantiation site that includes the same headers. Phase 2 ADL operates in a world where header-provided names are "globally visible" across TUs.

Modules introduce a distinction between exported names (declared with export) and non-exported names. A non-exported declaration from a module is reachable inside that module's implementation but is not visible to importers. This creates a new failure mode: a function that ADL would find via the traditional header model may be invisible at the instantiation context when names come from a module instead.

cpp
// geo.cppm — a module
export module geo;

export struct Vec2 { double x, y; };
export double dot(Vec2 a, Vec2 b);     // exported — ADL can find this

double dot_internal(Vec2 a, Vec2 b);   // NOT exported — invisible to importers
cpp
// algo.cppm — imports geo
import geo;

export template<typename T>
double magnitude_sq(T v) {
    return dot(v, v);  // phase 2 ADL: finds geo::dot (exported) — OK
    // dot_internal(v, v);  // would NOT be found by ADL — not exported from geo
}

When a module provides a type and its associated free functions, all functions that should participate in ADL for that type must be exported. Non-exported helpers are invisible to callers, even through ADL, because the standard defines ADL's search in terms of names that are visible (not merely reachable) in the associated namespaces of the argument types.

Module partitions add another layer. A module partition (e.g., geo:detail) can declare names that are visible within the module but not exported to importers. If a template in a partition-internal context uses ADL and the relevant overloads live in another partition that is not exported to the module interface, the behavior at the external instantiation site differs from the internal one. This subtle inconsistency is a known footgun in large module-based projects.

Importable header units (import "header.h") occupy a middle ground: they make all declarations in the header reachable at the instantiation context in a way that ordinary #include across translation units does not guarantee. The C++ standard explicitly states that if an importable header unit's declaration would produce a better ADL match than what was found, the program is ill-formed. This is the same "infinite TU" rule as before, now extended to cover header units as well.

The practical upshot: when migrating template-heavy code to modules, audit every free function in a type's associated namespace. If it participates (or should participate) in ADL-based generic algorithms, it must be exported. Hiding helpers by leaving them non-exported is correct for true implementation details, but doing so accidentally will silently change which overloads are reachable at instantiation sites outside the module.


See Also

  • Templates — template fundamentals and instantiation model
  • CRTP — pattern relying on dependent base member access
  • Concepts — C++20 constraints that reduce typename ceremony in some contexts
  • if constexpr — C++17 conditional compilation that still obeys phase 1 for non-dependent branches
  • Structured Bindings — another context where dependent name rules surface