Two-Phase Lookup & Dependent Names in C++

In C++, the issues caused by two-phase lookup and dependent names typically manifest in specific structural scenarios where the compiler cannot resolve a name until the template is instantiated. Let’s break down exactly where these lookup issues occur and how to fix them.

Template Metaprogramming Modern C++


Table of Contents

  1. Two-Phase Lookup & Dependent Names in C++
    1. Table of Contents
    2. 1. Dependent Base Classes (Inheritance)
    3. 2. Dependent Nested Types
    4. 3. Dependent Template Member Functions
    5. 4. Dependent Nested Class Templates
    6. Summary Checklist

1. Dependent Base Classes (Inheritance)

The Issue: Calling member functions or accessing member variables defined in a base class that depends on a template parameter (Base<T>).

Why it happens: In C++ templates, name lookup happens in two phases. Phase 1 occurs when the template is defined (before instantiation). Non-dependent names are looked up in Phase 1, but the compiler completely ignores dependent base classes during this phase because it doesn’t know what Base<T> might look like until T is known.

The Fix: Tell the compiler that the name depends on the instance by using this->, Base<T>::, or introducing the name with a using declaration.

template <typename T>
class Base {
protected:
    void doSomething() { /* ... */ }
    int count = 0;
};

template <typename T>
class Derived : public Base<T> {
public:
    void execute() {
        // ERROR: The compiler won't look in Base<T> during Phase 1.
        // doSomething(); 
        // count++;       

        // FIX: Make the names dependent so they are resolved in Phase 2.
        this->doSomething(); 
        this->count++;       
        
        // ALTERNATIVE FIX: Explicit scoping
        // Base<T>::doSomething();
    }
};
Deep Dive: Why doesn't the compiler just wait? If the compiler waited until Phase 2 to parse everything, it would defeat the purpose of early syntax checking. By parsing non-dependent names in Phase 1, C++ guarantees that basic syntax errors in templates are caught immediately, even if the template is never instantiated. Furthermore, specialized versions of Base<T> might not even have doSomething(), which is why the compiler refuses to assume it exists in Phase 1.

2. Dependent Nested Types

The Issue: Accessing a type defined inside a template parameter or dependent scope (e.g., iterators, inner structs like T::value_type).

Why it happens: The compiler encounters T::value_type. Because T is unknown, the compiler doesn’t know if value_type is a static member variable, a static function, or a type definition. By default, C++ assumes it is a value, not a type.

The Fix: Prefix the expression with the typename keyword to explicitly tell the compiler, “This identifier represents a type.”

#include <vector>
#include <iostream>

template <typename T>
void printFirstElement(const T& container) {
    // ERROR: Compiler assumes 'value_type' is a static member variable, 
    // so it tries to multiply it by 'firstElement' (pointer syntax).
    // T::value_type* firstElement; 

    // FIX: Use 'typename' to declare it as a type.
    typename T::value_type firstElement = *container.begin();
    
    std::cout << firstElement << '\n';
}

3. Dependent Template Member Functions

The Issue: Calling a member function that is itself a template on a dependent object (e.g., obj.foo<int>() where obj depends on T).

Why it happens: When parsing obj.foo<int>() in Phase 1, the compiler doesn’t know if foo is a template. Therefore, it misinterprets the < symbol as a “less-than” arithmetic operator rather than the start of a template argument list.

The Fix: Use the .template (or ->template) disambiguator to tell the compiler that the following name is a template.

struct MyPrinter {
    template <typename U>
    void print(U value) {
        // ...
    }
};

template <typename T>
void processAndPrint(T obj) {
    // ERROR: Parses as: (obj.print) < (int) > (42) ... syntax error!
    // obj.print<int>(42); 

    // FIX: Use the .template disambiguator
    obj.template print<int>(42);
}

// Usage:
// processAndPrint(MyPrinter{});

4. Dependent Nested Class Templates

The Issue: Constructing or referencing a nested class template that depends on a template parameter (e.g., T::InnerTemplate<int>).

Why it happens: This is a combination of issues #2 and #3. The compiler defaults to assuming that nested names are values, and it defaults to assuming that < is the less-than operator.

The Fix: Use the typename keyword at the start, and the template keyword before the nested template name.

struct Context {
    template <typename U>
    struct Builder {
        U build() { return U{}; }
    };
};

template <typename T>
void constructAndRun() {
    // ERROR: Compiler doesn't know 'Builder' is a type OR a template.
    // T::Builder<int> myObj; 

    // ERROR: Compiler knows it's a type, but parsing fails at '<'.
    // typename T::Builder<int> myObj; 

    // FIX: Use both typename and template
    typename T::template Builder<int> myObj;
    
    int result = myObj.build();
}

Summary Checklist

Whenever you are writing a C++ template and you use an identifier tied to a template parameter (T), watch out for these syntax requirements to satisfy two-phase lookup:

  • Inheritance Access dependent base class members using this->member
  • Types Access dependent nested types using typename T::type
  • Templates Call dependent template methods using object.template method<U>()

This site uses Just the Docs, a documentation theme for Jekyll.