C++ ADL (Argument-Dependent Lookup): Namespaces & Operators

C++ ADL (Argument-Dependent Lookup): Namespaces & Operators

이 글의 핵심

ADL lets unqualified calls find functions in namespaces associated with argument types—essential for operators and the swap two-step.

What is ADL?

Also called Koenig lookup: when resolving a unqualified function call, the compiler also searches namespaces associated with the argument types.

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 finds MyLib::print
    print(p);  // no MyLib:: prefix needed
    
    MyLib::print(p);  // qualified also works
}

How it works

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: A::func
    B::func(x);   // qualified: B::func
}

std::cout and ADL

#include <iostream>

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

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

Practical examples

Example 1: Operators

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};
    
    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

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

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

Example 3: Comparisons

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"};
    
    if (r1 == r2) {
        std::cout << "equal" << std::endl;
    }
    
    if (r1 < r2) {
        std::cout << "r1 less" << std::endl;
    }
    
    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
        }
        s.stream << "]";
        return s;
    }
}

int main() {
    Serialization::Serializer s;
    Serialization::Point p1{10, 20};
    Serialization::Point p2{30, 40};
    
    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;
}

Lookup scope notes

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: not found in C
        // ADL searches namespaces associated with argument types (here A), not C
    }
}

Nested namespaces

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

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

Common pitfalls

Pitfall 1: Unexpected overload

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;
    
    process(d);      // ADL: MyLib::process
    ::process(d);    // global::process
}

Pitfall 2: Templates

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

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

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

Pitfall 3: using declarations

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
}

Pitfall 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;
    
    func(x);  // global::func (no ADL for int)
    
    MyLib::func(x);  // qualified
}

Disabling ADL (mostly)

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: MyLib::func
    (func)(x);    // no ADL: global::func
    ::func(x);    // global::func
}

Best practices

namespace MyLib {
    struct Point {
        int x, y;
    };
    
    Point operator+(const Point& a, const Point& b) {
        return {a.x + b.x, a.y + b.y};
    }
    
    void swap(Point& a, Point& b) noexcept {
        std::swap(a.x, b.x);
        std::swap(a.y, b.y);
    }
    
    std::ostream& operator<<(std::ostream& os, const Point& p) {
        return os << "(" << p.x << ", " << p.y << ")";
    }
}

// Avoid dumping operators in global namespace for library types

FAQ

Q1: When does ADL apply?

A:

  • Unqualified calls
  • Associated namespaces of argument types
  • Especially operators

Q2: Why does it exist?

A:

  • Less verbose than always qualifying
  • Natural operator syntax
  • Cleaner generic code

Q3: How to disable?

A:

  • (func)(x)
  • ::func(x)

Q4: Built-in types?

A: No associated namespace—no ADL.

Q5: Pitfalls?

A:

  • Surprise overload resolution
  • Name clashes—qualify when needed

Q6: Learning resources?

A:

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

  • Namespace guide
  • Operator overloading
  • Operator precedence

Practical tips

Debugging

  • Warnings first

Performance

  • Profile

Code review

  • Conventions

Practical checklist

Before coding

  • Right approach?
  • Maintainable?
  • Performance?

While coding

  • Warnings?
  • Edge cases?
  • Errors?

At review

  • Intent?
  • Tests?
  • Docs?

Keywords

C++, ADL, argument-dependent lookup, namespace, operators


  • Function objects
  • Namespace complete
  • Operator overloading
  • Operator precedence
  • User-defined literals