Skip to content
C++
Language
since C++98
Intermediate

Argument-Dependent Lookup (ADL)

ADL (Koenig lookup) extends unqualified name lookup to include the namespaces of function argument types, enabling ergonomic operator and free-function design.

Argument-Dependent Lookup (ADL)since C++98

When a function is called with an unqualified name, the compiler extends its search to include all namespaces associated with the types of the call's arguments, in addition to the normal scope chain.

Overview

Argument-dependent lookup β€” also called Koenig lookup β€” is one of the most consequential name-lookup rules in C++. It exists to solve a real design problem: namespaces group types together with the free functions and operators that operate on them, but callers should not need using directives or fully qualified names to invoke those functions naturally.

The rule is straightforward to state. When the compiler sees an unqualified function call f(a, b), it not only searches the enclosing scopes in the usual way β€” it also searches every associated namespace of each argument's type. The candidate sets from both searches are merged and fed into overload resolution.

The associated namespaces of a type are determined as follows:

  • Class type: the namespace containing the class definition, plus the associated namespaces of all direct and indirect base classes.
  • Template specialisation: the namespace of the primary template, plus the associated namespaces of every template argument type.
  • Pointer or reference type: the associated namespaces of the pointed-to or referenced type.
  • Function type: the associated namespaces of all parameter types and the return type.
  • Fundamental types: no associated namespaces.

ADL fires only on unqualified calls. A call spelled ns::f(x) suppresses ADL entirely β€” you get exactly the function you named and nothing else.

How Lookup Works in Practice

cpp
#include <iostream>

namespace geom {
    struct Point { double x, y; };

    std::ostream& operator<<(std::ostream& os, const Point& p) {
        return os << "(" << p.x << ", " << p.y << ")";
    }

    double distance(const Point& a, const Point& b);
}

int main() {
    geom::Point p{1.0, 2.0}, q{4.0, 6.0};

    // ADL finds geom::operator<< β€” no 'using namespace geom' required
    std::cout << p << "\n";

    // ADL finds geom::distance because p and q are geom::Point
    double d = distance(p, q);
}

Without ADL, neither call would compile unless geom was brought into scope explicitly. The types themselves carry the information needed to find the right functions.

The Two-Step Swap Pattern

The most important practical application of ADL is the idiomatic swap call in generic code. A type's namespace may define a swap overload that is far more efficient than the generic std::swap β€” and you want that version to be called:

cpp
template <typename T>
void my_algorithm(T& a, T& b) {
    using std::swap;   // backstop if no ADL candidate exists
    swap(a, b);        // ADL fires first; std::swap wins only if nothing better is found
}

Calling std::swap(a, b) directly suppresses ADL β€” you always get the generic version regardless of what the type's namespace provides. The using std::swap + unqualified-call pattern is the canonical way to invoke swap in generic contexts.

The same two-step applies to begin and end in pre-C++17 range code:

cpp
template <typename Range>
void process(Range& r) {
    using std::begin;
    using std::end;
    // ADL picks up range-specific begin/end if they exist; std:: versions are the fallback
    for (auto it = begin(r); it != end(r); ++it) { /* ... */ }
}

ADL and Operators

ADL is the mechanism that makes overloaded operators work across namespace boundaries without any using declarations:

cpp
namespace lib {
    struct Vec3 { float x, y, z; };

    Vec3 operator+(const Vec3& a, const Vec3& b) {   // C++98
        return {a.x + b.x, a.y + b.y, a.z + b.z};
    }

    // C++20: defining operator<=> generates operator== and ordering
    auto operator<=>(const Vec3&, const Vec3&) = default;  // C++20
}

void compute(lib::Vec3 u, lib::Vec3 v) {
    auto w = u + v;    // ADL finds lib::operator+
    if (u == v) { }    // C++20: ADL finds synthesised lib::operator==
}

For stream insertion/extraction, ADL is why std::cout << my_type works when operator<< is defined in my_type's namespace. The std::ostream argument does not contribute my_type's namespace β€” only the argument of the user-defined type does.

Hidden Friends

A function defined with friend inside a class body is injected into the enclosing namespace for ADL purposes but is not reachable by ordinary (non-ADL) lookup. This is the hidden-friend idiom, widely recommended for operators and swap overloads since it limits discoverability to call sites that already have a value of the right type:

cpp
namespace lib {
    class Token {
        int id_;
    public:
        explicit Token(int id) : id_{id} {}

        // Findable only via ADL β€” 'lib::operator==' does not exist as a named entity
        friend bool operator==(Token a, Token b) noexcept {  // noexcept since C++11
            return a.id_ == b.id_;
        }

        friend void swap(Token& a, Token& b) noexcept {      // C++11
            using std::swap;
            swap(a.id_, b.id_);
        }
    };
}

// Works β€” ADL finds hidden friend
lib::Token t1{1}, t2{2};
bool same = (t1 == t2);

// Would not compile β€” operator== not in lib:: as a regular name
// bool same2 = lib::operator==(t1, t2);

Hidden friends also reduce compilation times and overload-set noise because they do not pollute the namespace for callers without a Token in hand.

Common Pitfalls

Unintended candidates. ADL can find functions you did not intend to call. If a type's associated namespace contains a function with the right name and a compatible signature, it enters the overload set. This can produce ambiguous-call errors or, silently, the wrong function being called.

Templates and ADL timing. In a function template, dependent-name resolution defers ADL to instantiation time. The set of ADL candidates therefore changes with each instantiation β€” a source of hard-to-diagnose overload surprises and potential ODR issues when the same template is instantiated with different types in different translation units.

operator, and operator& hazard. ADL applies to every unqualified function call, including those spelled as operators. Overloading operator, or operator& in a namespace can cause ADL to inject those overloads into contexts far removed from the type definition, with confusing results.

Qualified calls silently suppress ADL. In generic code, writing std::begin(r) instead of the two-step pattern means range-library customisations are never found. This is wrong for customisation points and right when you explicitly want the standard-library version.

Member calls are immune. obj.method() never triggers ADL β€” only free-function calls with unqualified names participate.

Best Practices

  • Define free functions and operators in the same namespace as the types they operate on. ADL is the glue that connects them to callers.
  • Use hidden friends (friend defined in the class body) for operators, swap, and other per-type customisations. They restrict discovery to ADL and avoid namespace pollution.
  • Always use the two-step using std::swap; swap(a, b); pattern in generic code. Never call std::swap directly when operating on a type parameter.
  • When you need to suppress ADL β€” to lock down which overload is called β€” use a qualified name or wrap the argument in a cast.
  • In library templates, audit every unqualified call for inadvertent ADL exposure; prefer qualified calls when only the standard-library version is correct.

ADL with Template Arguments

When a function argument is a class template specialisation, its associated namespaces expand to include the namespaces of every type template argument β€” not just the specialisation's own namespace. Non-type template arguments (e.g. integer values) and template template arguments' enclosing namespaces are handled separately; only type arguments contribute additional namespaces.

cpp
namespace widgets {
    struct Color { int r, g, b; };
    void paint(Color);
}

namespace containers {
    template <typename T>
    struct Box { T value; };

    // No free 'paint' in containers::
}

// ADL on Box<widgets::Color> searches both containers:: (Box's namespace)
// and widgets:: (Color's namespace as a type template argument).
void example() {
    containers::Box<widgets::Color> b;
    paint(b.value);  // OK β€” ADL finds widgets::paint via the Color argument

    // Now with the Box itself:
    // If there were a free function taking Box<Color>, ADL would search
    // both containers:: and widgets:: when the call is unqualified.
}

This cascading effect is the reason the two-step swap idiom works even for generic containers. When you write:

cpp
template <typename T>
void my_sort(std::vector<T>& v) {
    using std::swap;
    swap(v[0], v[1]);   // ADL searches std:: (from vector) AND T's namespace
}

ADL examines std:: because std::vector<T> is in std, and it examines T's namespace because T is a type template argument of std::vector. If T has a swap in its namespace, it wins over std::swap via overload resolution. Both namespaces are in play simultaneously.

The Abseil C++ style guide (Tip 49) notes that typedef and using aliases are expanded to their source types before associated namespaces are determined β€” so bspace::AliasForA (a typedef for aspace::A) causes ADL to search aspace, not bspace.

Inline Namespaces (C++11)

If an associated namespace is an inline namespace, its enclosing namespace is automatically added to the set too, and vice versa: if an enclosing namespace directly contains an inline namespace, the inline namespace is added. This is how versioned library namespaces (e.g. lib::v2::Type) can expose their types to ADL through the outer lib:: namespace.

ADL and Symmetric Binary Operators

Binary operators like +, -, ==, and < often need to work when the left-hand and right-hand operands are in different namespaces, or when one operand is from the standard library and the other is a user type. ADL handles this by searching the associated namespaces of all arguments simultaneously.

The key insight is that the overload must be findable from at least one operand's namespace:

cpp
namespace units {
    struct Metres { double value; };
    struct Feet   { double value; };

    // Operator found via ADL on either the Metres or Feet argument
    Metres operator+(Metres a, Feet b) {
        return {a.value + b.value * 0.3048};
    }
}

void compute() {
    units::Metres m{1.0};
    units::Feet   f{3.0};
    auto result = m + f;   // ADL finds units::operator+ from either operand
}

Where things break down: if the two operand types live in different namespaces and neither namespace defines the operator, ADL cannot find it even if the global namespace has an operator that looks compatible:

cpp
namespace left  { struct L {}; }
namespace right { struct R {}; }

// Defined in global namespace β€” ADL will NOT find this:
int operator+(left::L, right::R) { return 0; }

void bad() {
    left::L l;
    right::R r;
    // l + r;  // ERROR β€” ADL searches left:: and right::, not ::
}

The fix is to place the operator in one of the type's own namespaces. This is one reason Abseil recommends defining all operators adjacent to the type definitions in the same namespace β€” it makes ADL reliable and predictable.

Mixed-Type Comparisons and std::rel_ops

A practical consequence shows up with mixed-type equality. If operator== is a member function of A, it cannot be found by ADL when B is the left-hand side. Member functions never participate in ADL β€” only namespace-scope free functions do. This is why symmetric operators should be non-member functions, ideally hidden friends:

cpp
namespace geom {
    struct Point {
        double x, y;

        // Hidden friend: found via ADL on either argument, not via ordinary lookup
        friend bool operator==(const Point& a, const Point& b) noexcept {
            return a.x == b.x && a.y == b.y;
        }
    };
}

// Works from both directions β€” ADL finds the hidden friend on either operand
geom::Point p{1, 2}, q{3, 4};
bool eq1 = (p == q);   // ADL on p β†’ finds geom::operator==
bool eq2 = (q == p);   // ADL on q β†’ same

C++20's rewritten comparison expressions mean the compiler can also reverse operands when looking for operator== or operator<=>, but the operator still needs to be findable by ADL from at least one side.

The Poison Pill Technique

A poison pill is a = delete overload placed in a namespace to block ADL from finding a wider, unconstrained function. When overload resolution encounters a deleted function, the call is ill-formed β€” which, counterintuitively, is exactly the goal: it prevents a more general function in another namespace from being silently selected.

The technique was formally introduced in the C++20 Ranges design (described in P0970). The problem it solved: std::ranges::begin(E) should never call the unconstrained std::begin overloads when E is an rvalue, because those take const& and silently extend the lifetime. To block that path, the implementation was specified as if a context included:

cpp
// Poison pill β€” this deleted overload is more-specialised than
// the constrained std::begin for rvalue cases, making the call ill-formed
// rather than silently falling back to std::begin.
template <class T>
void begin(T&&) = delete;

With this poison pill visible during ADL, a call like begin(std::vector<int>()) prefers the deleted overload over std::begin and is rejected as ill-formed. Only user-supplied, non-member begin functions that take by-value or by-lvalue-reference pass through.

The same mechanism was applied to end, rbegin, rend, size, and similar customisation points. (The WG21 paper P2602 later found that most pills had outlived their usefulness and proposed removing them β€” all except iter_swap, where the unconstrained std::iter_swap still poses a real risk.)

Poison Pills for Library Extension Points

You can apply the same technique in your own library to prevent accidental use of unrelated free functions:

cpp
namespace mylib {
    // Primary customisation point
    template <typename T>
    void process(T& t);

    // Poison pill: blocks unqualified 'process' calls from falling through
    // to functions found only via ordinary (non-ADL) lookup.
    // Users must define 'process' in the same namespace as their type.
    template <typename T>
    void process(T&&) = delete;
}

Any type whose namespace does not provide a process overload will get a hard error from the deleted overload rather than silently calling an unrelated function.

Niebloids: Suppressing ADL at the Call Site (C++20)

The std::ranges algorithms go a step further than poison pills: they suppress ADL entirely at their own call site. The standard specifies that the entities in std::ranges "are not found by argument-dependent name lookup" and "when found by unqualified name lookup, inhibit ADL." In practice, every standard library implements this by making each algorithm a function object (a niebloid), not a function template:

cpp
// Conceptual structure of std::ranges::copy (simplified)
namespace std::ranges {
    struct copy_fn {
        template <std::input_iterator I, std::sentinel_for<I> S,
                  std::weakly_incrementable O>
        requires std::indirectly_copyable<I, O>
        constexpr auto operator()(I first, S last, O result) const;

        template <std::input_range R, std::weakly_incrementable O>
        requires std::indirectly_copyable<std::iterator_t<R>, O>
        constexpr auto operator()(R&& r, O result) const;
    };

    inline constexpr copy_fn copy{};   // an object, not a function template
}

Because unqualified name lookup finds copy as an object (not a function or function template), ADL is never triggered. This prevents std::copy from silently beating std::ranges::copy via the "more specialised" tiebreaker when iterators from std are in scope.

The practical consequence for users: std::ranges::find, std::ranges::sort, and their siblings cannot be hijacked by ADL the way ordinary functions can. You can safely write using namespace std::ranges; and then call copy(...) without worrying that an iterator type's associated namespace will redirect the call to the old std::copy.

ADL in Range-Based For Loops

Range-based for relies on unqualified begin and end calls, which means ADL is always in play. When the compiler expands for (auto x : r) it performs two unqualified calls β€” begin(r) and end(r) β€” after first trying member r.begin() and r.end(). If the member forms are absent, the free-function forms are sought through ADL in the namespaces associated with the type of r.

This means placing begin and end free functions in the same namespace as a type is sufficient to make that type work with range-based for without touching std:::

cpp
// C++11
namespace geo {
    struct Polygon {
        struct Vertex { double x, y; };
        Vertex verts[8];
        int count;
    };

    // Free begin/end in the same namespace β€” ADL finds these automatically
    const Polygon::Vertex* begin(const Polygon& p) { return p.verts; }
    const Polygon::Vertex* end(const Polygon& p)   { return p.verts + p.count; }
}

void iterate(const geo::Polygon& poly) {
    for (const auto& v : poly) {  // expands to: begin(poly) … end(poly)
        // ADL searches geo:: via poly's type, finds geo::begin and geo::end
    }
}

The member-function check happens first β€” if r.begin() is well-formed, the free-function ADL path is never reached. But for types where adding members is inconvenient (e.g. C arrays, third-party structs, or types defined in headers you don't own), the free-function ADL path is the cleanest solution.

Note that std::ranges::begin (C++20) does not use the raw ADL path β€” it is a CPO with its own prioritised dispatch logic described in the next section. Classic range-based for still uses the unqualified ADL mechanism defined by the core language, so both paths coexist.

Customisation Point Objects (CPOs)

CPOs replace raw ADL for the std::ranges customisation points because ADL alone is too permissive β€” it cannot enforce constraints or prioritise overloads consistently. A CPO is a statically-allocated function object whose operator() encodes a strict priority chain: try a member form first, then an ADL-found free function, then a standard fallback, and reject everything else with a static_assert or constraint failure.

The std::ranges::begin CPO illustrates the chain:

cpp
// C++20 β€” conceptual implementation (not verbatim standard wording)
namespace std::ranges {
    struct begin_fn {
        // Priority 1: decay-copy of member .begin() if well-formed
        template <typename T>
        requires requires(T& t) { t.begin(); }
        constexpr auto operator()(T& t) const
            noexcept(noexcept(t.begin()))
        { return t.begin(); }

        // Priority 2: ADL-found free begin(t) in associated namespaces
        template <typename T>
        requires (!requires(T& t) { t.begin(); }) &&
                  requires(T& t) { begin(t); }
        constexpr auto operator()(T& t) const
            noexcept(noexcept(begin(t)))
        { return begin(t); }
    };

    inline constexpr begin_fn begin{};
}

The critical difference from a plain ADL call is that std::ranges::begin is itself an object, so it does not participate in ADL when called. A user who writes std::ranges::begin(r) or captures the CPO as auto b = std::ranges::begin; cannot accidentally have a free function in some associated namespace hijack the call.

std::swap was retrofitted in C++20 to behave as a CPO as well. The old two-step idiom (using std::swap; swap(a, b);) remains correct, but code that calls std::ranges::swap(a, b) directly gets the CPO behaviour β€” constraint checking, noexcept propagation, and no silent ADL hijacking:

cpp
// C++20
#include <concepts>

namespace eng {
    struct Buffer {
        void* data;
        std::size_t size;

        // ADL-findable swap β€” std::ranges::swap will prefer this
        friend void swap(Buffer& a, Buffer& b) noexcept {
            using std::swap;
            swap(a.data,  b.data);
            swap(a.size,  b.size);
        }
    };
}

void example(eng::Buffer x, eng::Buffer y) {
    std::ranges::swap(x, y);  // CPO: finds eng::swap via ADL, verifies noexcept
}

The lesson for library authors: defining a free swap (or begin/end) in your type's namespace satisfies both the legacy ADL two-step and the modern CPO β€” no change is needed on the type side to benefit from CPO-based callers.

ADL and Friend Functions

The hidden-friend idiom makes friend functions the safest mechanism for per-type free-function customisation, because they are visible through ADL but invisible to ordinary unqualified lookup. A function defined as a friend inside a class body is injected into the enclosing namespace as a free function, but it has no entry in that namespace's ordinary lookup table β€” it can only be found when ADL specifically searches that namespace for a given argument type.

operator<< is the canonical example where this matters. Defining stream-insertion as a non-friend free function in the namespace exposes it to any code that does using namespace mylib;, making it part of every overload set that sees the namespace β€” including contexts where your type is nowhere in sight. A hidden friend avoids that:

cpp
// C++11
#include <ostream>

namespace fmt {
    class Colour {
        unsigned char r_, g_, b_;
    public:
        constexpr Colour(unsigned char r, unsigned char g, unsigned char b)
            : r_{r}, g_{g}, b_{b} {}

        // Hidden friend: found by ADL when a Colour argument is present,
        // but 'fmt::operator<<' is NOT a name you can look up directly.
        friend std::ostream& operator<<(std::ostream& os, Colour c) {
            return os << '#'
                      << std::hex
                      << static_cast<int>(c.r_)
                      << static_cast<int>(c.g_)
                      << static_cast<int>(c.b_);
        }
    };
}

void print(fmt::Colour c) {
    std::cout << c << '\n';   // ADL on c finds the hidden friend; works fine

    // Would fail β€” fmt::operator<< is not an ordinary name:
    // fmt::operator<<(std::cout, c);
}

A second benefit is reduced compile-time ambiguity. If operator<< is a regular namespace-scope function, every overload-resolution step that searches fmt:: considers it as a candidate, even for types that are completely unrelated to Colour. As a hidden friend, it is only a candidate when fmt::Colour is directly involved, cutting down the candidate set and speeding up compilation in large codebases.

Friend functions defined inside a class body also interact cleanly with class templates. The friend is a non-template function specialised for the exact instantiation, which avoids the complex partial-ordering rules that arise when you try to write a function template overloading on a class template parameter:

cpp
// C++17
namespace ds {
    template <typename T>
    class Stack {
        T* data_;
        std::size_t size_;
    public:
        // Each instantiation of Stack<T> gets its own non-template swap β€”
        // no template partial ordering needed, and it hides cleanly.
        friend void swap(Stack& a, Stack& b) noexcept {
            using std::swap;
            swap(a.data_, b.data_);
            swap(a.size_, b.size_);
        }
    };
}

ADL with using Declarations

The using std::swap + unqualified-swap pattern is the canonical two-step that allows a type-specific swap to win over the generic fallback, and it only works because the unqualified call triggers ADL. The using declaration does two things: it introduces std::swap into the local scope as a fallback candidate, and it signals the intent to use an unqualified call so that ADL can search the argument's namespace first.

This two-step generalises to any customisation point that follows the same convention:

cpp
// C++11
template <typename T>
void generic_swap(T& a, T& b) {
    using std::swap;  // step 1: bring in the fallback
    swap(a, b);       // step 2: unqualified call β€” ADL fires before std::swap is considered
}

namespace wire {
    struct Packet {
        unsigned char* buf;
        std::size_t    len;

        // Efficient swap β€” no dynamic allocation needed
        friend void swap(Packet& a, Packet& b) noexcept {
            using std::swap;
            swap(a.buf, b.buf);
            swap(a.len, b.len);
        }
    };
}

wire::Packet p, q;
generic_swap(p, q);  // ADL finds wire::swap; std::swap is never instantiated

Without the using declaration, the fallback must be spelled explicitly (std::swap(a, b)), which suppresses ADL. Without the unqualified call, ADL never fires and the type-specific overload is ignored. Both halves of the two-step are load-bearing.

The same pattern applies to begin/end lookups in pre-C++20 range utilities:

cpp
// C++14
template <typename Range>
auto range_size(const Range& r) {
    using std::begin;
    using std::end;
    // ADL finds range-specific begin/end if available; std:: versions are the fallback
    return std::distance(begin(r), end(r));
}

A subtlety: using declarations introduce names from a specific namespace into the current scope, while using namespace directives flood the whole namespace. For customisation-point two-steps, always prefer using std::swap (a declaration) rather than using namespace std; (a directive) β€” the directive's names have lower priority than those found by ADL and can produce surprising ties during overload resolution.

See Also

  • ADL Customization Points β€” designing extension points that clients satisfy via ADL
  • Niebloids β€” C++20 callable objects in <algorithm> that intentionally suppress ADL to prevent customisation-point hijacking
  • Overload Pattern β€” composing callable types, tightly related to overload-set construction