84 Advanced The Hundred
The Function You Never Imported
you have been relying on this since your first hello world
#include <iostream>
namespace geo {
struct Point {
int x, y;
};
void describe(Point p) { std::cout << "point at (" << p.x << ", " << p.y << ")\n"; }
} // namespace geo
int main() {
geo::Point p{3, 4};
describe(p);
std::operator<<(std::cout, "hello, fully qualified\n");
}
Run it. Does it even compile?
Answer
Yes — both lines. It prints point at (3, 4) and hello, fully qualified,
even though main never wrote geo:: or using for describe.
Why¶
When a call is unqualified — describe(p), not geo::describe(p) —
argument-dependent lookup (ADL) makes the compiler search not just the enclosing
scopes but also the namespaces of the argument types. p is a geo::Point, so
geo::describe is found with no import at all. You exploit this in every program:
std::cout << "hi" prints the text only because ADL finds the free
operator<<(ostream&, const char*) in namespace std — the last line of the demo is
that exact call with its full name spelled out. Without ADL the line would still
compile: member-only lookup falls back to basic_ostream::operator<<(const void*),
so std::cout.operator<<("hi") prints the pointer's address (something like
0x402010) instead of hi. The flip side is that ADL can silently pull in an
unintended overload from an argument's namespace. Deliberately inviting ADL is also
what the classic two-step using std::swap; swap(a, b); is for: the unqualified call
lets ADL find a type's own swap, with std::swap as the fallback.
The fix¶
Nothing here is broken — but when you want lookup on a leash, qualify the call, and when you want ADL on purpose, use the two-step:
geo::describe(p); // qualified: lookup goes exactly where you point
using std::swap; swap(a, b); // ADL first, std::swap as the fallback
Takeaway: for unqualified calls, a function effectively lives in the namespace of its argument types — lookup follows the arguments home.
Try it: g++ -std=c++17 main.cpp -o demo && ./demo