C++ / NAMESPACES, HEADERS, AND BUILDS
Argument-dependent lookup and its surprises
Predict which function an unqualified call picks by listing a type's associated namespaces, and recognize what silently switches ADL off.
What you will learn
- Derive associated namespaces from argument types, their bases, and their template args
- Put a type's operators and free functions in the type's own namespace
- Call swap and begin unqualified after a using-declaration to keep ADL in play
- Recognize the ADL blockers: same-named member, local declaration, qualified call
Understanding Argument-dependent lookup and its surprises
An unqualified call like f(x) is resolved from two candidate sets that are merged before overload resolution runs. The first comes from ordinary lexical lookup: block scope, then enclosing classes, then enclosing namespaces. The second comes from argument-dependent lookup, which for every argument collects that type's associated namespaces — the namespace the type is declared in, the namespaces of its base classes, and, for a class template specialization, the namespaces of its template arguments — and searches each of them for the name. That is the whole reason std::cout << p finds an operator<< you wrote in namespace geo: the type geo::Point brought geo along with it.
The mental model worth keeping is that operations travel with their type. operator<< cannot be a member of std::ostream because you do not own std::ostream, and generic code cannot qualify the call because it does not know your namespace, so ADL makes the argument type itself the address book. This is also why a friend function defined inside a class body is usable at all: it is a member of the enclosing namespace but invisible to ordinary lookup, so ADL is its only entry point, which is exactly what the hidden-friend idiom exploits to keep a name out of overload sets until an argument of the right type shows up.
The two halves interact in ways that bite. ADL is switched off the instant ordinary lookup finds a class member, a block-scope function declaration, or a name that is not a function at all, so adding an unrelated member called size breaks an unqualified size(c) inside that class, and a global int begin; breaks begin(c). In the other direction the candidate set grows silently: std::vector<my::Thing> associates both std and my, so a call can become ambiguous when someone adds an overload in a namespace you never mentioned. Qualifying the call removes ADL from the picture completely, which is the fix when you want one specific function and a bug when you wanted a customization hook.
<iostream>
<vector>
namespace geo {
struct Point {
int x, y;
// Hidden friend: a namespace-scope function that only ADL can find.
friend std::ostream& operator<<(std::ostream& os, const Point& p) {
return os << '(' << p.x << ", " << p.y << ')';
}
};
int manhattan(const Point& p) { return p.x + p.y; }
template <class C>
void describe(const C& c) {
std::cout << "geo::describe, " << c.size() << " points\n";
}
} // namespace geo
int main() {
geo::Point p{3, 4};
// Arguments associate std (ostream) and geo (Point), so the friend
// declared inside geo::Point becomes a candidate.
std::cout << p << '\n';
// No "geo::" and no using-directive: geo is searched because p lives there.
std::cout << manhattan(p) << '\n';
// vector<geo::Point> associates std AND geo: template arguments
// contribute their namespaces too.
std::vector<geo::Point> pts{p, p};
describe(pts);
}
An unqualified call also searches the namespaces of its arguments' types, which makes a function's namespace part of that type's public interface.
Worked examples
A member of the same name switches ADL off
Shows that once ordinary lookup finds a class member, argument-dependent lookup never runs.
<iostream>
namespace lib {
struct Tag {};
void ping(Tag) { std::cout << "lib::ping\n"; }
void pong(Tag) { std::cout << "lib::pong\n"; }
}
struct Client {
void ping(int) { std::cout << "Client::ping\n"; }
void run() {
lib::Tag t;
// ping(t); // error: Client::ping is found first, so ADL never runs
lib::ping(t); // qualify to reach the free function
pong(t); // no member 'pong', so ADL adds lib and finds it
}
};
int main() {
Client c;
c.run();
}
Example explained
Line 1The commented-out ping(t) fails because unqualified lookup stops at Client::ping, and finding a class member suppresses ADL entirely.
Line 2The resulting diagnostic talks about converting Tag to int, not about a missing function, which is why this bug reads so badly.
Line 3lib::ping(t) compiles because qualification names the namespace directly, bypassing lookup surprises and ADL alike.
Line 4pong(t) compiles because nothing named pong is in scope, so ADL adds lib, the namespace of Tag.
The using std::swap two-step
Shows how an unqualified call plus a using-declaration lets a type supply its own swap while keeping a generic fallback.
<iostream>
<utility>
<vector>
namespace fast {
struct Buffer {
std::vector<int> data;
};
// Declared in Buffer's namespace, so unqualified calls can find it.
void swap(Buffer& a, Buffer& b) {
std::cout << "fast::swap\n";
a.data.swap(b.data);
}
}
template <class T>
void exchange_pair(T& a, T& b) {
using std::swap; // fallback candidate, and it does not block ADL
swap(a, b);
}
int main() {
fast::Buffer x{{1, 2}}, y{{9}};
exchange_pair(x, y);
std::cout << x.data.size() << " " << y.data.size() << "\n";
int i = 1, k = 2;
exchange_pair(i, k);
std::cout << i << " " << k << "\n";
}
Example explained
Line 1using std::swap; is a using-declaration, which is explicitly exempt from the rule that a block-scope function declaration kills ADL.
Line 2For Buffer the candidates are std::swap<Buffer> and fast::swap; the non-template exact match wins, so the diagnostic line prints.
Line 3Writing std::swap(a, b) instead would never put fast::swap in the candidate set, and the type's own swap would be silently skipped.
Line 4For int, ADL contributes nothing because fundamental types have no associated namespace, so the std::swap fallback runs quietly.
A base class drags in its namespace
Shows that a namespace you never named can win a call, while a closer namespace is not even considered.
<iostream>
namespace core {
struct Base {};
void save(const Base&) { std::cout << "core::save\n"; }
}
// Doc lives in the global namespace, and nobody wrote using namespace core.
struct Doc : core::Base {};
namespace app {
void save(const Doc&) { std::cout << "app::save\n"; }
}
int main() {
Doc d;
save(d); // ADL adds core, the namespace of the base class
app::save(d); // app is not associated with Doc, so it must be qualified
}
Example explained
Line 1save(d) compiles although no save is visible at global scope: base classes contribute their namespaces, so core is searched.
Line 2core::save(const Base&) is viable for a Doc argument through the derived-to-base conversion.
Line 3app::save is the better match for Doc but never enters the candidate set, because app is not an associated namespace of Doc.
Line 4Moving Doc into namespace app would flip the winner, which shows that namespace placement is a design decision, not cosmetics.
Important notes
ADL applies only when the callee is written as a plain unqualified name; ns::f(x), a call through a function pointer, and a call on a functor object all bypass it, which is why library internals qualify their own calls.
In a template, the ordinary-lookup half of a dependent unqualified call is fixed at the template's definition point while the ADL half runs at the point of instantiation, so an overload declared later in your file can still be found, but only if it sits in an associated namespace.
Common mistakes
Writing std::swap(a, b) or std::begin(c) inside generic code: the qualification disables ADL, so a type's own overload is never considered and you silently get the generic one.
Defining operator<< for my::Point in the global namespace: your own std::cout << p compiles, but copying into a std::ostream_iterator<my::Point> fails to compile, because inside namespace std only ADL applies and the global namespace is not associated with my::Point.
Giving a class a member whose name matches a free function the class calls unqualified: ADL is switched off and the compiler reports a bad argument conversion instead of a missing overload, sending you hunting in the wrong place.
Try it yourself
Change, predict, then run
Put struct Vec { double x, y; }; and a free double length(const Vec&) inside namespace plot, then call length(v) unqualified from main with no using-directive and confirm it compiles. Now add int length = 0; at global scope and explain the error that appears.
Open the C++ workspaceCheck your understanding
Class Report has a member function render(const Layout&). Inside another member function of Report you write render(t), where t is a viz::Table and viz::render(const viz::Table&) exists. What happens?
- viz::render is chosen, since it is an exact match while the member would need a conversion.
- The call is ambiguous, because Report::render and viz::render are equally viable.
- Compile error: ordinary lookup finds Report::render, which suppresses ADL, so viz::render is never a candidate.
- viz::render is found, but only because viz::Table is taken by const reference.
Show answer
Finding a class member through ordinary unqualified lookup switches ADL off completely, so the only candidate is Report::render and the compiler complains that viz::Table does not convert to Layout. The first option is tempting because overload resolution normally prefers the better match, but resolution can only rank candidates that lookup produced, and viz::render never got in; qualifying the call as viz::render(t) fixes it.