C++ ADL | "Argument Dependent Lookup" Guide

C++ ADL | "Argument Dependent Lookup" Guide

이 글의 핵심

ADL (Koenig lookup): unqualified calls find operators in associated namespaces—core to std::swap and custom types.

What is ADL?

Argument-dependent lookup (ADL), also called Koenig lookup, extends ordinary name lookup for unqualified function and operator names: the compiler also searches associated namespaces of the argument types. That is why you can call print(p) for MyLib::Point p without writing MyLib::print—the type Point pulls in its namespace for overload resolution.

ADL applies to function call syntax (including operators). It does not replace normal lookup; it adds candidate functions from associated namespaces and classes, then overload resolution picks the best match.

namespace MyLib {
    struct Point {
        int x, y;
    };
    
    void print(const Point& p) {
        std::cout << "(" << p.x << ", " << p.y << ")" << std::endl;
    }
}

int main() {
    MyLib::Point p{10, 20};
    
    // ADL: Automatically finds MyLib::print
    print(p);  // No need for MyLib::
    
    // Explicit call is also possible
    MyLib::print(p);
}

How it works

In func(x) where x has type A::X, the compiler considers:

  1. Names found by usual unqualified lookup (including declarations visible in the current scope).
  2. Names in associated namespaces of A::X—here, namespace A—and associated classes (e.g. base classes, class templates).

If both A::func and B::func could match, overload resolution decides. The example below shows that only A::func is a candidate for func(x) because x is A::X; B::func is found only when you qualify the call.

namespace A {
    struct X {};
    void func(X) {
        std::cout << "A::func" << std::endl;
    }
}

namespace B {
    void func(A::X) {
        std::cout << "B::func" << std::endl;
    }
}

int main() {
    A::X x;
    
    func(x);      // ADL: Calls A::func
    B::func(x);   // Explicit: Calls B::func
}

std::cout and ADL

For the stream insertion operator (spelled operator followed by two less-than signs in source), the left operand is std::ostream& and the right operand is your type. Using cout syntax with your Point is a function call with two arguments, so ADL can find the overload declared with Point in MyLib. Define stream operators in the same namespace as the user-defined type.

#include <iostream>

namespace MyLib {
    struct Point {
        int x, y;
    };
    
    // Automatically found via ADL
    std::ostream& operator<<(std::ostream& os, const Point& p) {
        return os << "(" << p.x << ", " << p.y << ")";
    }
}

int main() {
    MyLib::Point p{10, 20};
    
    // ADL: Automatically finds MyLib::operator<<
    std::cout << p << std::endl;
}

Practical Examples

Example 1: Operator Overloading

namespace Math {
    struct Vector2 {
        float x, y;
    };
    
    Vector2 operator+(const Vector2& a, const Vector2& b) {
        return {a.x + b.x, a.y + b.y};
    }
    
    Vector2 operator-(const Vector2& a, const Vector2& b) {
        return {a.x - b.x, a.y - b.y};
    }
    
    Vector2 operator*(const Vector2& v, float scalar) {
        return {v.x * scalar, v.y * scalar};
    }
    
    std::ostream& operator<<(std::ostream& os, const Vector2& v) {
        return os << "Vector2(" << v.x << ", " << v.y << ")";
    }
}

int main() {
    Math::Vector2 v1{1.0f, 2.0f};
    Math::Vector2 v2{3.0f, 4.0f};
    
    // ADL: Automatically finds Math::operator+
    auto v3 = v1 + v2;
    auto v4 = v1 - v2;
    auto v5 = v1 * 2.0f;
    
    std::cout << v3 << std::endl;
    std::cout << v4 << std::endl;
    std::cout << v5 << std::endl;
}

Example 2: swap function

namespace MyLib {
    struct BigObject {
        std::vector<int> data;
        
        BigObject(size_t size) : data(size) {}
    };
    
    // Custom swap (ADL)
    void swap(BigObject& a, BigObject& b) noexcept {
        a.data.swap(b.data);
        std::cout << "MyLib::swap called" << std::endl;
    }
}

int main() {
    MyLib::BigObject obj1(1000);
    MyLib::BigObject obj2(2000);
    
    // ADL: Calls MyLib::swap
    using std::swap;  // fallback
    swap(obj1, obj2);
}

Example 3: Comparison Operators

namespace Data {
    struct Record {
        int id;
        std::string name;
    };
    
    bool operator==(const Record& a, const Record& b) {
        return a.id == b.id;
    }
    
    bool operator!=(const Record& a, const Record& b) {
        return !(a == b);
    }
    
    bool operator<(const Record& a, const Record& b) {
        return a.id < b.id;
    }
}

int main() {
    Data::Record r1{1, "Alice"};
    Data::Record r2{2, "Bob"};
    
    // ADL: Automatically finds Data::operator==
    if (r1 == r2) {
        std::cout << "Equal" << std::endl;
    }
    
    if (r1 < r2) {
        std::cout << "r1 is smaller" << std::endl;
    }
    
    // std::sort also uses ADL
    std::vector<Data::Record> records = {r2, r1};
    std::sort(records.begin(), records.end());
}

Example 4: Serialization

namespace Serialization {
    struct Serializer {
        std::ostringstream stream;
        
        std::string str() const {
            return stream.str();
        }
    };
    
    struct Point {
        int x, y;
    };
    
    Serializer& operator<<(Serializer& s, const Point& p) {
        s.stream << "{x:" << p.x << ",y:" << p.y << "}";
        return s;
    }
    
    Serializer& operator<<(Serializer& s, const std::vector<Point>& points) {
        s.stream << "[";
        for (size_t i = 0; i < points.size(); i++) {
            if (i > 0) s.stream << ",";
            s << points[i];  // ADL: Recursive call to Serializer::operator<<
        }
        s.stream << "]";
        return s;
    }
}

int main() {
    Serialization::Serializer s;
    Serialization::Point p1{10, 20};
    Serialization::Point p2{30, 40};
    
    // ADL: Calls Serialization::operator<<
    s << p1;
    std::cout << s.str() << std::endl;
    
    std::vector<Serialization::Point> points = {p1, p2};
    Serialization::Serializer s2;
    s2 << points;
    std::cout << s2.str() << std::endl;
}

ADL search scope

ADL searches namespaces associated with the argument types, not the namespace where the call appears. Inside C::test, an unqualified func(x) would look at C via normal lookup, but ADL for A::X adds namespace A—not C. If func exists only in B, the unqualified call fails. This is why free functions meant to be found via ADL should live in the same namespace as the types they operate on.

namespace A {
    struct X {};
}

namespace B {
    void func(A::X) {
        std::cout << "B::func" << std::endl;
    }
}

namespace C {
    void test() {
        A::X x;
        // func(x);  // Error: Cannot find func in C
        // ADL only searches the namespace of A::X (A)
    }
}

Nested namespaces

For a type like Outer::Inner::X, associated namespaces include both Outer::Inner and enclosing namespaces such as Outer (per the standard’s rules for associated namespaces). In practice, place friend-like free functions in the innermost namespace that defines the type so ADL stays predictable.

namespace Outer {
    namespace Inner {
        struct X {};
        
        void func(X) {
            std::cout << "Inner::func" << std::endl;
        }
    }
}

int main() {
    Outer::Inner::X x;
    
    // ADL: Finds Outer::Inner::func
    func(x);
}

Common Issues

Issue 1: Unintended Function Call

namespace MyLib {
    struct Data {};
    
    void process(Data) {
        std::cout << "MyLib::process" << std::endl;
    }
}

void process(MyLib::Data) {
    std::cout << "global::process" << std::endl;
}

int main() {
    MyLib::Data d;
    
    // ADL: Calls MyLib::process (might not be intended)
    process(d);
    
    // Explicit call
    ::process(d);  // global::process
}

Issue 2: Templates and ADL

namespace MyLib {
    struct X {};
    
    void func(X) {
        std::cout << "MyLib::func" << std::endl;
    }
}

template<typename T>
void call(T value) {
    func(value);  // ADL works
}

int main() {
    MyLib::X x;
    call(x);  // Calls MyLib::func
}

Issue 3: using Declaration and Conflicts

namespace A {
    struct X {};
    void func(X) { std::cout << "A::func" << std::endl; }
}

namespace B {
    void func(A::X) { std::cout << "B::func" << std::endl; }
}

int main() {
    using B::func;
    
    A::X x;
    func(x);  // Ambiguous! A::func vs B::func
}

Issue 4: Built-in Types

void func(int) {
    std::cout << "func(int)" << std::endl;
}

namespace MyLib {
    void func(int) {
        std::cout << "MyLib::func(int)" << std::endl;
    }
}

int main() {
    int x = 10;
    
    // ADL does not work (int has no namespace)
    func(x);  // Calls global::func
    
    // Explicit call is required
    MyLib::func(x);
}

Disabling ADL

Wrapping the function name in parentheses in a call suppresses ADL: (func)(x) performs only ordinary lookup, so it can pick a global func even when MyLib::func would win with ADL. Prefer ::func(x) when you want the global namespace explicitly. Use this sparingly—usually fixing overload sets or namespace layout is clearer.

namespace MyLib {
    struct X {};
    void func(X) { std::cout << "MyLib::func" << std::endl; }
}

void func(MyLib::X) {
    std::cout << "global::func" << std::endl;
}

int main() {
    MyLib::X x;
    
    func(x);      // ADL: Calls MyLib::func
    (func)(x);    // Disables ADL: Calls global::func
    ::func(x);    // Explicit: Calls global::func
}

Best Practices

namespace MyLib {
    struct Point {
        int x, y;
    };
    
    // ✅ Define operators in the same namespace
    Point operator+(const Point& a, const Point& b) {
        return {a.x + b.x, a.y + b.y};
    }
    
    // ✅ Define swap in the same namespace
    void swap(Point& a, Point& b) noexcept {
        std::swap(a.x, b.x);
        std::swap(a.y, b.y);
    }
    
    // ✅ Stream operator
    std::ostream& operator<<(std::ostream& os, const Point& p) {
        return os << "(" << p.x << ", " << p.y << ")";
    }
}

// ❌ Do not define in the global namespace
// Point operator+(const MyLib::Point& a, const MyLib::Point& b) { ... }

FAQ

Q1: When does ADL work?

A:

  • During function calls
  • Searches the namespace of the argument
  • Works for operator overloading

Q2: Why is it needed?

A:

  • Removes the need for explicit namespace qualification
  • Enables natural use of operators
  • Simplifies template code

Q3: How to disable it?

A:

  • Use parentheses: (func)(x)
  • Use explicit calls: ::func(x)

Q4: Does it work for built-in types?

A: ADL does not work for built-in types as they do not belong to any namespace.

Q5: What should I watch out for?

A:

  • Unintended function calls
  • Potential name conflicts
  • Consider explicit calls when necessary

Q6: Where can I learn more about ADL?

A:

  • “Effective C++”
  • “C++ Templates”
  • cppreference.com

Here are some related posts on this topic:

  • C++ namespace | “Avoiding Name Conflicts” Complete Guide
  • C++ Operator Overloading | Guide to redefining +, -, *, and stream operators
  • C++ Operator Precedence | “Operator Precedence” Guide

Practical Tips

Tips you can apply directly in your projects.

Debugging Tips

  • Check compiler warnings first when issues arise
  • Reproduce the issue with simple test cases

Performance Tips

  • Avoid optimization without profiling
  • Set measurable performance metrics first

Code Review Tips

  • Anticipate common feedback during code reviews
  • Follow your team’s coding conventions

Practical Checklist

Use this checklist to ensure proper application of this concept in your projects.

Before Writing Code

  • Is this technique the best solution for the current problem?
  • Can your team understand and maintain this code?
  • Does it meet performance requirements?

While Writing Code

  • Have you resolved all compiler warnings?
  • Have you considered edge cases?
  • Is error handling appropriate?

During Code Review

  • Is the intent of the code clear?
  • Are there sufficient test cases?
  • Is the code properly documented?

Use this checklist to reduce mistakes and improve code quality.


Keywords Covered in This Post (Related Search Terms)

Search for C++, ADL, name-lookup, namespace, operator, and similar terms to find more helpful content related to this post.