Name Lookup
How the C++ compiler associates every name in source code with the declaration that introduced it, before overload resolution begins.
Name Lookupsince C++98Name lookup is the compiler procedure that associates every name encountered in a translation unit with the declaration β or, for overloaded functions, the set of declarations β that introduced it.
Overview
Every identifier in a C++ program must be tied to a declaration before the program can compile. Name lookup performs that binding. It runs before overload resolution and before access checking: candidates are first gathered by lookup, then filtered by overload resolution, and only then are access rules applied. Understanding this ordering is essential because it explains why a private member can still participate in overload resolution (and why the error says "access" rather than "not found").
The lookup procedure divides into two branches depending on whether the name is qualified or unqualified.
- A qualified name appears to the right of
:::std::cout,Base::reset,::global_fn. - An unqualified name appears without a preceding
:::cout,reset,global_fn.
Qualified Name Lookup
Qualified lookup searches exactly the scope named on the left of ::. It does not consider enclosing namespaces, does not perform ADL, and stops as soon as it finds the name in the designated scope (or fails with an error).
namespace outer {
void f(int);
namespace inner {
void f(double);
void g() {
outer::f(1); // finds outer::f(int) only β inner::f not considered
outer::inner::f(1); // finds inner::f(double)
}
}
}For class member access, qualified lookup traverses the class and its base-class hierarchy, respecting declaration order. One important use is deliberate bypass of virtual dispatch:
struct Base {
virtual void process();
};
struct Derived : Base {
void process() override;
void run() {
process(); // unqualified β virtual dispatch, calls Derived::process
Base::process(); // qualified β calls Base::process directly, no dispatch
}
};Calling Base::process() via qualified lookup is the canonical pattern for invoking a base implementation from an override without triggering polymorphism.
Unqualified Name Lookup
Unqualified lookup starts at the point of use and moves outward through enclosing scopes: block β enclosing blocks β function scope β class scope (for member functions) β enclosing namespaces β global namespace. The search halts at the first scope that contains at least one declaration of the name.
int threshold = 100;
namespace config {
int threshold = 50;
void check(int v) {
// Lookup finds config::threshold first; global threshold is shadowed
if (v > threshold) { /* uses 50 */ }
if (v > ::threshold) { /* uses 100 β qualified, bypasses shadowing */ }
}
}Argument-Dependent Lookup (ADL)
For function names in a call expression, unqualified lookup is extended by ADL (Koenig lookup). The compiler collects all namespaces associated with the types of the function arguments β including the namespace of the type itself, its enclosing namespaces, and namespaces of its template arguments β and searches those too. This extension applies only to unqualified function calls.
ADL is what makes operator overloading and customization-point idioms work without littering call sites with namespace qualifications:
#include <iostream>
#include <utility>
namespace geom {
struct Vec2 { double x, y; };
std::ostream& operator<<(std::ostream& os, Vec2 v) {
return os << v.x << ' ' << v.y;
}
void swap(Vec2& a, Vec2& b) noexcept { // noexcept since C++11
using std::swap;
swap(a.x, b.x);
swap(a.y, b.y);
}
}
void example() {
geom::Vec2 u{1.0, 2.0}, v{3.0, 4.0};
std::cout << u << '\n'; // ADL finds geom::operator<<
swap(u, v); // ADL finds geom::swap; std::swap available as fallback
}ADL does not apply to variable names, type names, or namespaces β only to function names in call expressions. Qualifying the call (e.g., geom::swap(u, v)) disables ADL for that call.
Function Names vs. All Other Names
The semantics of what lookup returns differ by name category.
Function names: Lookup gathers every visible declaration from each scope it searches β it does not stop at the first scope. ADL can add further candidates from associated namespaces. The entire candidate set is then handed to overload resolution.
All other names (variables, types, namespaces, enumerators): Lookup must produce exactly one declaration, or multiple declarations of the same entity (e.g., an extern variable declared in multiple headers). Finding two distinct entities with the same name is ill-formed.
extern int counter; // declaration 1
extern int counter; // declaration 2 β same entity, OK
struct Result { int code; };
int Result = 0; // type/non-type in same scope β special rule applies (see below)Type/Non-Type Hiding (The Struct Hack)
C++ preserves a C compatibility rule: within a single scope, a struct/class/union/enum tag name can coexist with a variable, function, or enumerator of the same name. When both exist, ordinary lookup finds the non-type name; the type name is hidden and can only be accessed with an elaborated type specifier.
struct status { int code; };
void handle() {
int status = 200; // hides the struct name in this scope
// status s; // error: 'status' here means the int
struct status s; // OK: elaborated-type-specifier reaches the hidden type
s.code = status;
}This rule also applies when a function name and a class name collide:
namespace io {
struct stream { /* ... */ };
void stream(); // hides struct stream from plain lookup
// stream s; // error: stream names the function
struct stream s; // OK
}Two-Phase Lookup in Templates
Inside a function template body, lookup is split into two phases to handle the distinction between names that depend on template parameters and those that do not.
Phase 1 runs at the point of template definition. Names that are non-dependent (do not involve template parameters) are looked up immediately, using the enclosing context at definition time.
Phase 2 runs at instantiation. Dependent names β those that involve template parameters β are looked up using both the definition context and the instantiation context (including ADL over the deduced argument types).
namespace lib {
struct Token {};
void validate(Token); // must be declared before instantiation, not before definition
}
template <typename T>
void pipeline(T t) {
validate(t); // dependent call β phase-2 lookup, ADL finds lib::validate
}
void run() {
pipeline(lib::Token{}); // instantiation triggers phase-2 lookup
}The most common mistake is relying on phase-2 lookup for a function that is not findable via ADL β it won't be found even if declared before the instantiation point.
Common Pitfalls
Shadowing without intent: An inner declaration silently shadows an outer one. -Wshadow catches this.
int count = 0;
void accumulate(const std::vector<int>& items) {
for (int count : items) { // shadows outer count β outer is never incremented
// work with local count
}
// outer count still 0
}Inherited names in templates: Unqualified lookup inside a class template does not search dependent base classes (phase-1 lookup; base type unknown at definition). Make names dependent explicitly.
template <typename T>
struct Base { void helper(); };
template <typename T>
struct Derived : Base<T> {
void run() {
// helper(); // error: not found β Base<T> not searched in phase 1
this->helper(); // OK: makes the name dependent, deferred to phase 2
Base<T>::helper(); // OK: qualified lookup
}
};ADL interference: Any name introduced into an associated namespace after a template is defined can be picked up at instantiation. This makes templates sensitive to what third-party code places in the same namespace as their argument types β a real concern when writing generic library code.
Qualified Lookup: All Three Forms
The :: operator introduces three distinct lookup modes depending on what appears to its left.
Global scope (::name): An empty left-hand side means the search is restricted to the global namespace only. No enclosing namespace, no class scope, no ADL. This is the escape hatch when a local or namespace-scope declaration shadows a global name.
int open = 42; // global variable
namespace fs {
int open = 0;
void reset() {
int open = -1; // local variable
fs::open = 0; // namespace-qualified: finds fs::open
::open = 99; // global-scope qualified: finds the global int
// open = 1; // unqualified: finds local, shadows both others
}
}Namespace scope (Ns::name): Search is restricted to the named namespace. The compiler does not travel into enclosing namespaces from there, does not apply ADL, and does not search nested namespaces unless you use a chained :: (e.g., Ns::Inner::name). The single-namespace search is exhaustive: all declarations visible in that scope are considered, including names brought in by using declarations inside the namespace.
Member access (obj.name and ptr->name): These are not :: expressions, but they do trigger a form of qualified lookup. The compiler searches the class of obj (or the pointee class of ptr) and then every base class in declaration order. Overload resolution follows once all candidates from the class hierarchy are collected. Virtual dispatch is still applied at runtime regardless of which overload is chosen at compile time β unless you use the Base::method() qualified-call form, which forces a direct, non-virtual call to the named base-class implementation.
struct Animal {
virtual std::string sound() const { return "..."; }
};
struct Dog : Animal {
std::string sound() const override { return "woof"; }
std::string describe() const {
return sound(); // unqualified β virtual, calls Dog::sound at runtime
// return Animal::sound(); // qualified β static dispatch, always Animal::sound
}
};ADL: Associated Namespaces in Full Detail
The brief description in the overview covers the common case. The complete rules for what constitutes the associated namespace set of a type are more elaborate, and they matter for generic library design.
For a given type T used as a function argument, the associated namespaces are built up as follows:
- Fundamental type (
int,double, pointer, etc.): no associated namespaces. - Class type (including unions): the namespace directly enclosing the class definition; the associated namespaces of every direct and indirect base class; if the class is itself a template specialisation, the associated namespaces of every type template argument and every class template argument (non-type template arguments such as integers contribute nothing).
- Enumeration type: the namespace directly enclosing the enum definition.
- Pointer or reference type: the associated namespaces of the pointed-to or referenced type.
- Array type: the associated namespaces of the element type.
- Function type: the associated namespaces of all parameter types and the return type.
In every case, the enclosing namespace of any associated class is also added. Inline namespaces are treated as equivalent to their enclosing namespace for this purpose β adding an inline namespace automatically adds its parent and vice versa, which is how versioned library namespaces expose their types transparently.
namespace shapes {
namespace v2 {
inline namespace stable {
struct Polygon { int sides; };
}
}
}
namespace render {
void draw(shapes::v2::stable::Polygon p); // lives in render::
}
// ADL on Polygon: associated namespaces include shapes::v2::stable,
// shapes::v2 (parent of the inline ns), and shapes:: (enclosing ns).
// render:: is NOT associated β draw() must be qualified here:
void test(shapes::v2::stable::Polygon p) {
render::draw(p); // must qualify; render:: is not an associated namespace
}Template arguments extend associated namespaces transitively. A std::vector<MyNs::MyType> argument associates both std:: (for vector) and MyNs:: (for MyType). This is how the two-step swap idiom works even for standard containers holding user types.
What ADL Cannot Do
ADL finds names only in associated namespaces, not by following using directives visible inside those namespaces. If ns:: contains using namespace other;, that does not pull other:: into the ADL set. This is a subtle but important restriction: the set is determined by the types, not by what those types' namespaces happen to import.
ADL also never fires for:
- Qualified calls (
ns::f(x)β the qualification pins the lookup scope) - Object-member calls (
obj.f()β member lookup, not free-function lookup) - Calls where the name was found via a
usingdeclaration in the immediate scope that names a specific set of overloads (in that case theusing-declared set is complete and ADL is suppressed)
Two-Phase Lookup: Mechanics and Consequences
The two-phase model exists because a function template body is parsed at definition time but cannot be fully resolved until the template arguments are known. The compiler therefore splits name lookup into two passes.
Phase 1 β Definition Time
At the point where the template is written, the compiler resolves every non-dependent name. A name is non-dependent if its meaning cannot change regardless of what template arguments are eventually supplied. These names are looked up immediately in the scope surrounding the template definition using the ordinary scope chain. If a non-dependent name is not found at definition time, the program is ill-formed even if it would have been found at an instantiation point.
void helper(); // must exist here for phase-1 lookup to find it
int global_counter = 0; // non-dependent: found at definition time
template <typename T>
void process(T val) {
helper(); // non-dependent call β resolved in phase 1
++global_counter; // non-dependent variable β resolved in phase 1
val.method(); // dependent call β deferred to phase 2
}Phase 2 β Instantiation Time
When the template is instantiated with concrete arguments, the compiler resolves all dependent names. Dependent names include any name that directly involves a template parameter (e.g., T, sizeof(T), member access on a T value). For dependent function calls, both ordinary lookup and ADL are performed at the instantiation point, using both the definition context and the instantiation context.
This is why declaring a function after a template definition but before an instantiation can affect lookup for dependent names, while the same declaration would have no effect on non-dependent names.
namespace lib {
struct Event {};
}
template <typename T>
void dispatch(T t) {
handle(t); // dependent β phase-2 lookup with ADL at instantiation
}
namespace lib {
void handle(Event e); // declared after template definition β still found!
}
void run() {
dispatch(lib::Event{}); // instantiation here; phase 2 finds lib::handle via ADL
}The Dependent Base Class Problem
A particularly common confusion involves templates that inherit from a dependent base class β a base whose type is spelled in terms of a template parameter. At phase 1, the compiler does not know what Base<T> looks like, so it cannot search it for names. Unqualified names found in that base are invisible to phase-1 lookup.
template <typename T>
struct Base {
void helper();
int value = 0;
};
template <typename T>
struct Derived : Base<T> {
void run() {
// helper(); // ERROR β not found; Base<T> not searched in phase 1
// ++value; // ERROR β same reason
this->helper(); // OK β 'this->' makes the name dependent; deferred to phase 2
++this->value; // OK β same fix
Base<T>::helper(); // OK β qualified lookup, explicitly names the base
}
};this->helper() works because this has type Derived<T>*, which depends on T, making helper a dependent name and deferring its lookup to phase 2 where Base<T> is known. Base<T>::helper() works because the qualification explicitly identifies the scope to search.
Both fixes are semantically equivalent for virtual functions except for one difference: Base<T>::helper() suppresses virtual dispatch just as any qualified call does. Prefer this-> when you want polymorphic behaviour, Base<T>:: when you want to bypass it.
Name Hiding and Scope
C++ scope rules are strict: a name declared in an inner scope hides all declarations of the same identifier in every outer scope, regardless of type or signature. This is fundamentally different from Java and C#, where a subclass method does not hide unrelated overloads in the base class.
void report(int);
void report(double);
void report(const std::string&);
void process() {
void report(long); // local declaration β hides ALL three outer overloads
report(3.14); // calls the local report(long), not report(double)!
report(42); // also calls local report(long)
}Member Function Hiding in Class Hierarchies
The same rule applies across base and derived classes. A member function in a derived class hides all base-class members with the same name, even overloads with different signatures:
struct Base {
void draw(int colour);
void draw(float opacity);
void draw(const char* label);
};
struct Derived : Base {
void draw(double scale); // hides ALL Base::draw overloads
};
void use(Derived d) {
d.draw(1.0); // OK β calls Derived::draw(double)
// d.draw(42); // ERROR β Base::draw(int) is hidden
// d.draw("label"); // ERROR β Base::draw(const char*) is hidden
d.Base::draw(42); // OK β qualified lookup bypasses hiding
}The canonical fix is a using declaration that re-introduces the base-class overloads into the derived class scope:
struct Derived : Base {
using Base::draw; // imports all Base::draw overloads
void draw(double scale); // adds one more; all four are now visible
};After the using Base::draw; declaration, all three base overloads plus the derived one participate in overload resolution together. This pattern is essential for types that extend a base interface without dropping existing overloads, and for constructors (C++11 inheriting constructors: using Base::Base;).
Overload Sets: Lookup Before Resolution
A point that surprises many programmers is that overload resolution operates on the combined candidate set built by lookup, not on candidates found one scope at a time. For function names, unqualified lookup does not stop at the first scope that contains a match β it continues through all scopes and collects every visible declaration before handing the set to overload resolution.
void process(long); // namespace scope
struct Handler {
void process(int); // member function
void process(double);
void run(int n) {
process(n); // member lookup finds Handler::process(int) and
// Handler::process(double); namespace process(long) is
// hidden by the class scope β it does NOT participate
}
};The critical nuance: lookup searches outward through scopes, but stops when it finds at least one name in a scope. All overloads in that scope are collected, but scopes further out are not searched further. The namespace-scope process(long) above is hidden because the class scope has process declarations; the search stops there.
ADL works differently: it augments the candidate set with additional overloads found in associated namespaces, even if those namespaces are not in the normal lookup path. The augmented set is then subject to overload resolution as a whole. This means ADL can resolve ambiguities (by providing an exact match) or create them (by introducing an equally good candidate from an associated namespace).
namespace io {
struct Stream {};
void write(Stream&, int); // found via ADL
void write(Stream&, double); // also found via ADL
}
void write(io::Stream&, long); // namespace-scope, found via ordinary lookup
void test(io::Stream& s) {
write(s, 42); // candidates: io::write(Stream&,int), io::write(Stream&,double),
// ::write(Stream&,long)
// io::write(Stream&,int) is the best match β selected
}C++20 Modules and Name Lookup
Modules, introduced in C++20, add a new axis to name lookup: whether a name is exported or module-private. The key rules:
Exported names are visible to importers. A name declared with export in a module interface unit is found by ordinary name lookup in any translation unit that imports the module.
// geometry.cppm (module interface unit)
export module geometry;
export struct Point { double x, y; }; // visible to importers
export Point midpoint(Point a, Point b); // visible to importers
struct InternalHelper { /* ... */ }; // NOT exported β module-private
void detail_impl(); // NOT exported β module-private// main.cpp
import geometry;
void run() {
Point p{1, 2}, q{3, 4}; // OK β Point is exported
Point m = midpoint(p, q); // OK β midpoint is exported
// InternalHelper h; // ERROR β not exported, not visible
}Unexported names are not found by ADL. This is the subtlest module lookup rule. Even if the associated namespace of a function argument is defined within a module, unexported names in that namespace are invisible to ADL from outside the module.
// widgets.cppm
export module widgets;
export namespace widgets {
struct Button { int id; };
// Exported β found normally and via ADL:
export void render(Button b);
}
namespace widgets {
// NOT exported β invisible to outside importers, even via ADL:
void internal_reset(Button b);
}// ui.cpp
import widgets;
void update(widgets::Button b) {
render(b); // OK β ADL finds exported widgets::render
// internal_reset(b); // ERROR β not exported; ADL cannot find it
}This behaviour is intentional: unexported names represent implementation details that the module does not wish to expose. The module boundary enforces encapsulation at the name-lookup level, not just via access control. Unlike static functions in headers or anonymous namespaces, module-private names are genuinely invisible to all outside translation units, including those that share an associated namespace with the argument type.
One practical consequence: generic code that relies on ADL to find a customisation point (such as swap or serialize) must ensure those customisation-point functions are exported from the module. Leaving them unexported silently breaks the customisation for callers that import the module, with no diagnostic at the module author's site.
See Also
- Argument-Dependent Lookup (ADL) β how associated namespaces extend unqualified function lookup
- Two-Phase Lookup β splitting lookup between template definition and instantiation
- Dependent Names β names in templates whose meaning depends on template parameters
- Conflicting Declarations β when lookup finds incompatible or ambiguous declarations