C++ ADL | "Argument Dependent Lookup" 가이드

C++ ADL | "Argument Dependent Lookup" 가이드

이 글의 핵심

C++ ADL(Argument Dependent Lookup)은 함수 호출 시 인자의 네임스페이스에서 함수를 찾는 규칙입니다. operator 오버로딩과 함께 사용되며, 네임스페이스 설계의 핵심 개념입니다.

ADL이란?

ADL(Argument Dependent Lookup) 은 함수 이름을 찾을 때, 글자 그대로 인자(Argument)와 연관된 네임스페이스까지 함께 뒤지는 규칙입니다. 우편물을 보낼 때 수신인 이름만 보지 않고 주소가 속한 동·구까지 함께 찾아가는 것과 비슷합니다. 그래서 std::를 매번 붙이지 않아도 print(p)처럼 자연스럽게 쓸 수 있고, operator<<를 타입과 같은 네임스페이스에 두는 관용이 성립합니다.

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: MyLib::print 자동 찾음
    print(p);  // MyLib:: 불필요
    
    // 명시적 호출도 가능
    MyLib::print(p);
}

작동 원리

같은 이름 func가 전역과 다른 네임스페이스에 모두 있을 때, 인자 x의 타입이 정의된 네임스페이스 쪽 후보가 ADL로 끌려옵니다. 아래에서는 func(x)A::func를 고르는 이유를 확인할 수 있습니다.

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);   // 명시적: B::func 호출
}

std::cout과 ADL

operator<<(std::ostream&, const Point&)MyLib에 두면, std::cout << p를 쓸 때 스트림은 std에, Point는 MyLib 있어도 ADL이 MyLib의 연산자를 찾아 줍니다. 사용자 정의 타입의 출력 연산자를 전역에 흩뿌리지 않고도 쓸 수 있는 이유입니다.

#include <iostream>

namespace MyLib {
    struct Point {
        int x, y;
    };
    
    // ADL로 자동 찾음
    std::ostream& operator<<(std::ostream& os, const Point& p) {
        return os << "(" << p.x << ", " << p.y << ")";
    }
}

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

실전 예시

예시 1: 연산자 오버로딩

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

예시 2: swap 함수

namespace MyLib {
    struct BigObject {
        std::vector<int> data;
        
        BigObject(size_t size) : data(size) {}
    };
    
    // 커스텀 swap (ADL)
    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);
    
    // ADL: MyLib::swap 호출
    using std::swap;  // fallback
    swap(obj1, obj2);
}

예시 3: 비교 연산자

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: Data::operator== 자동 찾음
    if (r1 == r2) {
        std::cout << "같음" << std::endl;
    }
    
    if (r1 < r2) {
        std::cout << "r1이 작음" << std::endl;
    }
    
    // std::sort도 ADL 사용
    std::vector<Data::Record> records = {r2, r1};
    std::sort(records.begin(), records.end());
}

예시 4: 직렬화

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: Serializer::operator<< 재귀 호출
        }
        s.stream << "]";
        return s;
    }
}

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

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);  // 에러: C에서 func 못 찾음
        // ADL은 A::X의 네임스페이스(A)만 검색
    }
}

중첩 네임스페이스

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

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

자주 발생하는 문제

문제 1: 의도하지 않은 함수 호출

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: MyLib::process 호출 (의도와 다를 수 있음)
    process(d);
    
    // 명시적 호출
    ::process(d);  // global::process
}

문제 2: 템플릿과 ADL

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

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

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

문제 3: using 선언과 충돌

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);  // 모호함! A::func vs B::func
}

문제 4: 빌트인 타입

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 작동 안함 (int는 네임스페이스 없음)
    func(x);  // global::func 호출
    
    // 명시적 호출 필요
    MyLib::func(x);
}

ADL 비활성화

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);    // ADL 비활성화: global::func
    ::func(x);    // 명시적: global::func
}

모범 사례

namespace MyLib {
    struct Point {
        int x, y;
    };
    
    // ✅ 같은 네임스페이스에 연산자 정의
    Point operator+(const Point& a, const Point& b) {
        return {a.x + b.x, a.y + b.y};
    }
    
    // ✅ swap도 같은 네임스페이스
    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 << ")";
    }
}

// ❌ 전역 네임스페이스에 정의하지 말 것
// Point operator+(const MyLib::Point& a, const MyLib::Point& b) { ... }

FAQ

Q1: ADL은 언제 작동?

A:

  • 함수 호출 시
  • 인자의 네임스페이스 검색
  • 연산자 오버로딩

Q2: 왜 필요?

A:

  • 네임스페이스 명시 불필요
  • 연산자 자연스럽게 사용
  • 템플릿 코드 간결

Q3: 비활성화 방법?

A:

  • (func)(x) 괄호 사용
  • ::func(x) 명시적 호출

Q4: 빌트인 타입은?

A: ADL 작동 안함. 네임스페이스 없음.

Q5: 주의사항?

A:

  • 의도하지 않은 함수 호출
  • 이름 충돌 가능
  • 명시적 호출 고려

Q6: ADL 학습 리소스는?

A:

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

같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • C++ namespace | “이름 충돌 방지” 완벽 가이드
  • C++ 연산자 오버로딩 | ”+, -, *, <<” 재정의 가이드
  • C++ 연산자 우선순위 | “Operator Precedence” 가이드

관련 글

  • C++ 함수 객체 |
  • C++ namespace |
  • C++ 연산자 오버로딩 |
  • C++ 연산자 우선순위 |
  • C++ User-Defined Literals |