Skip to content

Instantly share code, notes, and snippets.

@MangaD
Last active December 25, 2025 12:48
Show Gist options
  • Select an option

  • Save MangaD/1fbad9b6f28776edc79707bbe711642a to your computer and use it in GitHub Desktop.

Select an option

Save MangaD/1fbad9b6f28776edc79707bbe711642a to your computer and use it in GitHub Desktop.
C++ Compilation Semantics: Name Lookup, Overload Resolution, and the Hidden Phases of Meaning

C++ Compilation Semantics: Name Lookup, Overload Resolution, and the Hidden Phases of Meaning

CC0

Disclaimer: ChatGPT generated document.

What you’re hearing described (“name-lookup phase”, “overload-resolution time”) is part of the front-end semantic pipeline a C++ compiler runs through to turn tokens into “this exact entity/function/template is meant here”.

Below is a fairly complete deep dive, split into:

  1. Standard translation phases (the “real” phases the standard names)
  2. Front-end semantic phases (the practical “priorities/timing” you’re asking about: lookup, ADL, overload resolution, template deduction, constraints, instantiation, etc.)
  3. Priority rules & tie-breakers (especially overload ranking)
  4. Two-phase lookup and dependent names (the big “timing” gotcha)
  5. Concrete examples showing what happens when

1) The standard “translation phases” (C++ compilation phases)

These are the phases in the C++ standard (often summarized as 8 phases; exact wording varies by edition but the structure is stable):

  1. Physical source file mapping / character set translation Source bytes → “universal character set” / implementation character set.

  2. Line splicing Backslash-newline sequences are removed (historically important for macros).

  3. Tokenization and preprocessing

    • The file is broken into preprocessing tokens and whitespace/comments.
    • Comments are removed (conceptually replaced by whitespace).
  4. Macro expansion + directive handling #include, #define, conditional compilation, pragma, etc. Result: a single preprocessed token stream (roughly).

  5. Character literals / string literal processing Escape sequences, concatenation of adjacent string literals, etc.

  6. Parsing into grammar structures Tokens → parse tree / AST shape. (Still not “meaning” — e.g., T::x might parse even if x doesn’t exist.)

  7. Semantic analysis This is where your “name lookup”, “overload resolution”, “template instantiation”, constraints, etc. live.

  8. Code generation / linking model (not fully specified by the standard) The standard stops being prescriptive, but compilers then do IR generation, optimization, object emission, and linking.

Your question is really about Phase 7, which is huge and internally staged.


2) The “semantic pipeline” compilers effectively run

Think of compilation as repeatedly answering: “What does this token sequence mean?” The major sub-problems (and the “timing” people refer to) are:

A. Build scopes and declarations

As the compiler parses, it introduces declarations into symbol tables (namespaces, classes, block scopes, etc.).

B. Name lookup (several kinds)

Given a name like f, the compiler determines the candidate declarations it could refer to.

Key lookup categories:

  • Unqualified lookup: f(...)
  • Qualified lookup: N::f, obj.f, T::f
  • Member lookup: inside classes, base classes, injected-class-names, etc.
  • Argument-dependent lookup (ADL): “Koenig lookup” adds candidates based on argument types
  • Using-directive effects: using namespace std; changes lookup sets (but with tricky rules)
  • Using-declaration effects: using std::swap; introduces specific overload sets

Lookup does not pick “the best overload”. It produces sets (or reports ambiguity / not found).

C. Form an overload candidate set

For a call expression f(a,b) after lookup:

  • If f denotes an overload set (or function template(s)), gather all possible overloads and template candidates.

  • Include:

    • non-templates
    • function templates
    • (sometimes) built-in candidates (e.g., operators)
    • (sometimes) rewriting candidates (C++20: <=>/relational rewriting, etc.)
    • for member calls, candidates might include base class members and hidden members depending on lookup and using declarations.

D. Template argument deduction (if templates are involved)

For each function template candidate:

  • Deduce template parameters from call arguments (or explicit <...>).
  • Substitute the deduced arguments into the function type.

This step can fail:

  • deduction failure
  • substitution failure (SFINAE)
  • constraints not satisfied (C++20 concepts)

Only successful ones remain viable.

E. Viability filtering

A candidate function is viable if:

  • it can be called with the given arguments
  • access control / deletion / constraints allow it
  • conversions exist for each parameter
  • special rules for ..., ref-qualifiers, implicit object parameter, etc.

F. Overload resolution ranking (“best viable function”)

Among viable candidates:

  • compute implicit conversion sequences for each argument
  • compare candidates pairwise
  • pick the best, or diagnose ambiguity

G. If chosen candidate is a template: instantiation timing

Even after choosing a function template specialization, the compiler may still defer instantiating the function body until needed (depending on ODR-use, inline, etc.). But the signature must be known to call it.

H. Final checks

  • access control
  • lifetime / temporary materialization rules
  • narrowing conversions (list-init)
  • constant evaluation
  • etc.

3) Name lookup in detail (the “phases” people mean)

3.1 Unqualified name lookup

For f in f(x):

  • Search the innermost scope outward:

    1. block scope
    2. function parameter scope
    3. class scope (if inside member function)
    4. enclosing namespaces
    5. global namespace
  • “Using declarations” inject names into a scope.

  • “Using directives” (using namespace N;) make names visible for lookup in a more subtle way: they don’t literally inject names; they affect how namespace scopes are searched.

Important “priority” rule:

  • A declaration found in an inner scope hides outer declarations of the same name for unqualified lookup (with several class/namespace nuances).

3.2 Qualified name lookup

For N::f:

  • Lookup is restricted to N and what N makes visible (including inline namespaces, using directives inside N, etc.).

For T::f where T is a type:

  • If T is a class, member lookup rules apply, including base classes.

3.3 Member lookup & hiding

Inside classes:

  • A derived class member named f hides all base class f overloads unless you do using Base::f;.
  • This is lookup-time hiding, before overload resolution.
struct B { void f(int); void f(double); };
struct D : B { void f(char*); }; // hides BOTH B::f overloads

3.4 Argument-dependent lookup (ADL)

For an unqualified function call f(x) (and certain operator calls), ADL adds candidates from associated namespaces/classes of the argument types.

Associated entities include:

  • the argument’s class type and its base classes
  • namespaces where those types are defined
  • for template specializations, associated namespaces of template arguments too

ADL is why swap(a,b) can find myns::swap when a is myns::X.

Crucial “priority” fact:

  • ADL augments the candidate set after ordinary unqualified lookup finds f (or fails to find it) — but it only applies in contexts where ADL is permitted (not in ::f(x); the leading :: suppresses ADL by forcing global qualified lookup).

3.5 Lookup vs overload resolution

Lookup answers: “Which declarations named f are considered?” Overload resolution answers: “Which of those declarations is the one being called?”

A lot of “why didn’t my overload get picked?” problems are actually lookup/hiding problems, not overload-ranking problems.


4) Overload resolution “priorities”: how the best function is chosen

Overload resolution is essentially a tournament with these main stages:

4.1 Candidate kinds

Candidates may be:

  • non-template functions
  • function template specializations (after deduction)
  • converting constructors / conversion functions (for some contexts)
  • built-in operator candidates
  • rewritten candidates (some operators)

4.2 Viability

Eliminate candidates that can’t be called.

4.3 Ranking by implicit conversion sequences (ICS)

For each argument, the compiler classifies the conversion needed:

  • Exact match (best)

    • identity
    • qualification adjustments
    • integral promotions (still “exact match” category-ish vs “promotion” depending on context; compilers/standard separate “Exact Match” and “Promotion” as ranks)
  • Promotion

    • char → int, float → double, etc.
  • Conversion

    • int → double, derived-to-base pointer, etc.
  • User-defined conversion

    • constructors / conversion operators (with a standard conversion before and/or after)
  • Ellipsis

    • match to ... (worst)

The standard defines comparison rules: “better conversion sequence” wins per parameter; the best viable function is one that is not worse for any argument and better for at least one (with many subtleties).

4.4 Tie-breakers and special rules

If conversion sequences tie, further rules may pick a winner, including:

  • Non-template vs template: non-template can be preferred if otherwise equal (but not always; partial ordering of templates is its own step).
  • More specialized template wins (partial ordering of function templates).
  • Ref-qualifiers / cv-qualifiers on member functions can matter via the implicit object parameter.
  • Initializer-list constructors are favored in list-initialization.
  • Narrowing is forbidden for list-initialization, which can remove candidates entirely.
  • Constraints (C++20): a constrained template may be preferred; “more constrained” can win if both viable and ordering rules apply.

4.5 Partial ordering of function templates

When two function template candidates are viable, the compiler decides which is more specialized by trying to deduce one from the other in a formal way.

This is why:

template<class T> void f(T);
template<class T> void f(T*);

calling f(p) with int* p prefers the pointer version.

4.6 Constraints (concepts) add a whole extra layer

For constrained templates:

  • First, constraints must be satisfied to be viable.
  • Then, if multiple are viable, the compiler can prefer the more constrained one (roughly: if one implies the other).

5) “Name lookup phase” timing: templates and two-phase lookup

This is the #1 reason people talk about “phases” in this context.

5.1 What “two-phase lookup” means

In templates, the compiler performs lookup in (at least) two conceptual times:

  1. Template definition time (first phase) Non-dependent names are looked up immediately, when the template is parsed/checked.

  2. Template instantiation time (second phase) Dependent names are looked up later, when the template is instantiated with concrete template arguments.

This is why the same code can compile or fail depending on whether a name is dependent.

5.2 Non-dependent vs dependent names

A name is dependent if it depends on a template parameter.

Example:

template<class T>
void g(T t) {
  f(t);     // dependent: argument type depends on T => ADL may find f later
  h();      // non-dependent: looked up now
}
  • h() must be found at template definition time. If there’s no h visible then, it’s an error (even if later you define h).
  • f(t) is dependent because t’s type depends on T; ADL can kick in at instantiation and find f in the namespace of T.

5.3 Dependent qualified names and the typename / template keywords

Because parsing and lookup happen before T is known, C++ needs disambiguators:

  • typename T::value_type tells the parser “this is a type”.
  • t.template foo<int>() tells it “foo is a template”.

These aren’t “style”; they’re required because at definition time the compiler can’t know what T::value_type refers to.

5.4 The classic trap: base class members in templates

Unqualified lookup in a class template does not automatically consider dependent base classes at definition time.

template<class T>
struct Base { void f(); };

template<class T>
struct Der : Base<T> {
  void g() { f(); } // ERROR: f not found in first phase
};

Fixes:

  • this->f(); (makes it dependent)
  • Base<T>::f(); (qualified, dependent)
  • using Base<T>::f; (bring into scope)

This is purely lookup timing.


6) Putting it together: “existing phases” you’ll hear in C++ discussions

People commonly refer to these “semantic phases” (not officially named by the standard as phases, but very real):

  1. Parsing (grammar)
  2. Early name binding / non-dependent lookup (template definition time)
  3. Type formation (build types, resolve declarators)
  4. Template argument deduction (for call candidates)
  5. Substitution & SFINAE (remove failing template candidates)
  6. Constraint checking (C++20)
  7. Overload resolution (best viable selection)
  8. Template instantiation (bodies, class members, variable templates)
  9. Constant evaluation (constexpr, immediate functions, consteval)
  10. ODR-use decisions (what must be instantiated/emitted)
  11. Access control / deleted function checks (some are earlier depending on context)
  12. Codegen

In reality, compilers interleave these, but the rules behave as if they happen in this kind of order.


7) A few “priority” gotchas worth memorizing

Lookup hiding beats overload resolution

If a derived class declares f, base overloads named f are not even candidates unless reintroduced.

using std::swap; swap(a,b); is deliberate

  • using std::swap; ensures ordinary lookup finds std::swap
  • then swap(a,b) allows ADL to add myns::swap as a better match This is a canonical “lookup + ADL” pattern.

List-initialization changes everything

T x{arg}; engages initializer-list constructors and bans narrowing, often altering viability.

::f(x) suppresses ADL

If you qualify with ::, you’re forcing lookup and typically turning off ADL for that call.

Dependent vs non-dependent decides “when” errors happen

If it’s non-dependent, you can get an error even if some instantiation would have made it valid.


If you want, I can go one level deeper and enumerate overload resolution for operators specifically (built-in candidates, rewritten candidates, user-defined operators, member vs non-member priority, operator<=> rewriting, etc.), because that’s where the rules get the most “phase-y”.


Operator Overload Resolution in C++: Built-ins, Rewriting, and Phase Ordering

Alright — this is where C++ stops feeling like a language and starts feeling like a logic puzzle designed by committee. I’ll go deep and systematic, because operator overload resolution has more phases, more synthetic candidates, and more rewriting than normal function calls.

We’ll cover:

  1. What makes operator resolution special

  2. Overall pipeline (the exact order things happen)

  3. Lookup rules for operators

  4. Candidate construction

    • member operators
    • non-member operators
    • built-in operators
    • rewritten candidates
  5. Viability and implicit object parameter

  6. Ranking and tie-breakers

  7. operator<=> and relational rewriting

  8. Conversion operators and operators

  9. Common traps and why they happen

  10. Mental model you can actually use


1. Why operator overload resolution is special

Unlike normal function calls:

f(a, b);

An operator expression like:

a + b

is not just syntax sugar for operator+(a, b).

Instead, the compiler:

  • synthesizes multiple kinds of candidates
  • performs lookup differently
  • adds built-in operators
  • creates rewritten operators (C++20+)
  • treats the left operand as an implicit object parameter for member operators
  • runs overload resolution after all of this

This makes operators the most phase-heavy part of the language.


2. The high-level pipeline (important)

For an operator expression like:

a @ b

the compiler does this exact conceptual sequence:

1. Determine operator kind and context
2. Collect operator function candidates
   a) Member operator candidates
   b) Non-member operator candidates (via lookup + ADL)
   c) Built-in operator candidates
   d) Rewritten operator candidates (C++20)
3. Filter viable candidates
4. Rank candidates (overload resolution)
5. Select best viable candidate or diagnose ambiguity

Every “why did this overload win?” question maps to one of these steps.


3. Lookup rules for operators

3.1 Operators are functions, but lookup is special

For a + b, the compiler looks for:

  • a.operator+(b) (member)
  • operator+(a, b) (non-member)

But lookup is not symmetric.


3.2 Member operator lookup

If the operator can be a member (most binary operators except = etc.):

  • The left operand’s class type is searched for operator@
  • Base classes are considered
  • Access control applies

Important:

Only the left operand participates in member lookup

struct A {
  A operator+(const A&) const;
};

A a, b;
a + b;   // OK
b + a;   // OK (same member)

But:

struct B {};

A a;
B b;
a + b;   // Member candidate exists
b + a;   // No member operator+ in B

3.3 Non-member operator lookup (ADL-heavy)

For operator+(a, b):

  • Unqualified lookup applies
  • Argument-dependent lookup is always used
  • Associated namespaces of both operands are considered

This is why non-member operators are often preferred for symmetric operators.


3.4 Qualification suppresses ADL

::operator+(a, b); // ADL suppressed

This often silently removes user-defined overloads.


4. Candidate construction (the heart of it)

4.1 Member operator candidates

For a @ b, member candidates are synthesized as:

a.operator@(b)

Conceptually rewritten as a function with an implicit object parameter:

operator@(A const* this, B)

Ref-qualifiers and cv-qualifiers apply here.


4.2 Non-member operator candidates

Found via normal lookup + ADL:

operator@(a, b)

These are ordinary functions.


4.3 Built-in operator candidates

Here’s the big surprise:

Built-in operators participate in overload resolution

Examples:

  • arithmetic on fundamental types
  • pointer arithmetic
  • comparisons
  • logical operators
  • bitwise operators

These are imaginary functions with fixed signatures.

Example for +:

int operator+(int, int);
double operator+(double, double);
T* operator+(T*, std::ptrdiff_t);

They’re only added if no user-defined candidate blocks them via better viability.


4.4 Rewritten operator candidates (C++20+)

This is where things get truly weird.

4.4.1 Relational operator rewriting

For <, >, <=, >=, the compiler may rewrite expressions into:

  • a < b
  • b < a
  • (a <=> b) < 0
  • (b <=> a) > 0

These rewritten forms become additional candidates.


5. Viability filtering (including implicit object parameter)

A candidate is viable only if:

  • All parameters can be matched
  • Access is allowed
  • Not deleted
  • Constraints satisfied (concepts)
  • For members: implicit object parameter binding is valid

5.1 Implicit object parameter rules

For a member operator:

struct X {
  int operator+(int) &;
};

The implicit object parameter is X&.

Thus:

X{} + 1; // ❌ temporary cannot bind to non-const lvalue ref

This affects overload ranking just like a normal parameter.


6. Ranking and tie-breakers (operator-specific)

After viability, normal overload resolution applies — but with extra structure.

6.1 Member vs non-member: no automatic priority

Important myth to kill:

Member operators are NOT preferred over non-members

The only difference is:

  • member operators use implicit object parameter
  • non-members treat both operands equally

Whichever yields better conversions wins.


6.2 Built-in vs user-defined

If a user-defined operator is viable and better than the built-in candidate, it wins.

Example:

struct X {};
X operator+(X, X);

X a, b;
a + b; // user-defined beats built-in (built-in not viable anyway)

6.3 Rewritten candidates compete equally

Rewritten candidates are not “fallbacks” — they fully participate.

This is why <=> can dominate relational operators.


7. operator<=> and relational rewriting (C++20)

7.1 The rule

If no direct overload of <, >, <=, >= is viable, the compiler tries:

a <=> b

And rewrites:

Expression Rewritten as
a < b (a <=> b) < 0
a > b (a <=> b) > 0
a <= b (a <=> b) <= 0
a >= b (a <=> b) >= 0

These rewritten forms create new operator candidates.


7.2 Consequence: defining <=> can silently change overload resolution

struct X {
  auto operator<=>(const X&) const = default;
};

X a, b;
a < b;   // works, even without operator<

And worse:

struct Y {
  int operator<(const Y&) const;
  auto operator<=>(const Y&) const;
};

Now overload resolution must choose between:

  • direct <
  • rewritten <=>

Subtle conversion differences can change which wins.


8. Conversion operators interacting with operators

Conversion operators introduce user-defined conversion sequences, which are ranked worse than standard conversions.

Example:

struct X {
  operator int() const;
};

X x;
x + 1;  // built-in int + int after conversion

But if there’s also:

X operator+(X, int);

That may win because:

  • one standard conversion vs
  • one user-defined conversion + built-in

9. Common traps (and why they happen)

9.1 “Why isn’t my base class operator found?”

Lookup-time hiding:

struct B { friend bool operator==(B, B); };
struct D : B {};

D d1, d2;
d1 == d2; // ❌ ADL doesn't consider B unless conversion happens

Fix: define operator in namespace, not as hidden friend, or add using declarations.


9.2 Asymmetric operators behaving asymmetrically

struct A {};
bool operator==(A, int);

A a;
a == 1; // OK
1 == a; // ❌ different lookup set

Because lookup starts from the operands’ types.


9.3 Temporaries + ref-qualified operators

struct X {
  bool operator==(const X&) &;
};

X{} == X{}; // ❌ no viable candidate

Implicit object parameter cannot bind.


10. The mental model (this actually works)

When debugging operator overloads, always ask:

  1. Which candidates exist?

    • member?
    • non-member via ADL?
    • built-in?
    • rewritten?
  2. Which are viable?

    • ref qualifiers?
    • access?
    • constraints?
  3. Which conversions are needed?

    • standard vs user-defined
    • implicit object parameter included
  4. Did rewriting introduce a better candidate?

    • especially <=>

If you answer those four, the compiler’s decision will stop feeling arbitrary.


If you want to go even deeper

We can still drill into:

  • operator = vs copy/move generation
  • assignment vs compound assignment operators
  • operator[] and multidimensional indexing
  • operator new/delete lookup and replacement rules
  • literal operators (operator"")
  • spaceship + defaulted comparison edge cases

Just say which rabbit hole you want next.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment