인자 의존 이름 찾기

인자 의존 이름 찾기 (ADL)

std::cout << "hello" 같은 코드를 쓸 때, 네임스페이스 한정 없이 operator<<가 어떻게 찾아지는지 궁금한 적 있나요? 함수 이름을 인자(argument)의 네임스페이스에서도 찾아주는 규칙 덕분인데요, 그게 바로 인자 의존 이름 찾기, 흔히 ADL(또는 Koenig lookup)이라고 불리는 거예요. 이 페이지에서 ADL이 정확히 언제, 어떻게 동작하는지 정리해 볼게요.

출처: cppreference

본문

ADL은 함수 호출 표현식에서 비한정 함수 이름을 찾는 규칙이에요. 오버로드된 연산자에 대한 암시적 함수 호출도 포함하죠. 이 함수 이름들은 일반적인 비한정 이름 찾기(usual unqualified name lookup)가 고려하는 스코프와 네임스페이스에 더해, 인자들의 네임스페이스에서도 찾아져요.

ADL 덕분에 다른 네임스페이스에 정의된 연산자도 사용할 수 있어요.

#include <iostream>

int main()
{
    std::cout << "Test\n"; // 전역 네임스페이스에 operator<<는 없지만, ADL이
                           // 왼쪽 인자가 std에 있으므로 std 네임스페이스를 조사해
                           // std::operator<<(std::ostream&, const char*)를 찾아요
    operator<<(std::cout, "Test\n"); // 같은 일을 함수 호출 문법으로

    // 하지만,
    std::cout << endl; // Error: "endl"은 이 네임스페이스에 선언돼 있지 않음.
                       // endl()에 대한 함수 호출이 아니므로 ADL이 적용되지 않음
    
    endl(std::cout); // OK: 함수 호출이므로 ADL이 std 네임스페이스를 조사하고
                     // 인자가 std에 있으므로 std::endl을 찾음
    
    (endl)(std::cout); // Error: "endl"은 이 네임스페이스에 선언돼 있지 않음.
                       // 부분 표현식 (endl)은 비한정 id(unqualified-id)가 아님
}

자세히

먼저, 일반 비한정 이름 찾기로 만들어진 조회 집합에 다음 중 하나가 포함되면 ADL은 고려되지 않아요.

  1. 클래스 멤버의 선언
  2. 블록 스코프의 함수 선언(using 선언은 제외)
  3. 함수나 함수 템플릿이 아닌 선언(예: 찾고 있는 함수 이름과 이름이 충돌하는 함수 객체나 변수)

그 외에는, 함수 호출 표현식의 각 인자마다 그 타입을 조사해서 조회에 추가할 '연관 네임스페이스와 연관 클래스의 집합'을 결정해요.

  1. 기본 타입(fundamental type) 인자(std::meta::info는 제외)(C++26부터)의 연관 집합은 비어 있어요.
  2. 클래스 타입(공용체 포함) 인자의 집합은 다음으로 이뤄져요. a) 클래스 자신 b) 클래스가 완전(complete)하다면, 직접·간접 기반 클래스 전부 c) 클래스가 다른 클래스의 멤버라면, 그 멤버를 둔 클래스 d) 집합에 추가된 클래스들의 가장 안쪽 enclosing 네임스페이스
  3. 클래스 템플릿 특수화 타입의 인자는 클래스 규칙에 더해 다음 연관 클래스·네임스페이스가 집합에 더해져요. a) 타입 템플릿 매개변수에 제공된 모든 템플릿 인자의 타입(비타입 템플릿 매개변수와 템플릿 템플릿 매개변수는 건너뜀) b) 템플릿 템플릿 인자가 멤버인 네임스페이스 c) 템플릿 템플릿 인자가 멤버인 클래스(클래스 멤버 템플릿이라면)
  4. 열거형 타입 인자는, 그 열거형 타입이 정의된 가장 안쪽 enclosing 네임스페이스가 집합에 추가돼요. 열거형 타입이 클래스 멤버라면 그 클래스도 집합에 추가돼요.
  5. T에 대한 포인터 타입이나 T의 배열에 대한 포인터 타입의 인자는 타입 T를 조사해서 그 연관 클래스·네임스페이스 집합을 더해요.
  6. 함수 타입 인자는 함수 매개변수 타입들과 함수 반환 타입을 조사해서 그 연관 집합을 더해요.
  7. 클래스 X의 멤버 함수 F에 대한 포인터 타입 인자는 함수 매개변수 타입들, 함수 반환 타입, 클래스 X를 조사해서 그 연관 집합을 더해요.
  8. 클래스 X의 데이터 멤버 T에 대한 포인터 타입 인자는 멤버 타입과 타입 X를 둘 다 조사해서 그 연관 집합을 더해요.
  9. 인자가 오버로드된 함수(또는 함수 템플릿) 집합의 이름이거나 그 주소를 구하는 표현식이라면, 오버로드 집합의 각 함수를 조사해서 그 연관 집합을 더해요.
    • 추가로, 오버로드 집합이 템플릿 id로 이름 붙여졌다면 그 타입 템플릿 인자와 템플릿 템플릿 인자 전부(비타입 템플릿 인자는 제외)를 조사해서 그 연관 집합을 더해요.

연관 클래스·네임스페이스 집합이 결정되면, 이 집합의 클래스들에서 찾은 선언은 (아래 2번에 있는 네임스페이스 수준 friend 함수·함수 템플릿을 제외하고) 이후 ADL 처리에서 버려져요.

일반 비한정 이름 찾기로 찾은 선언 집합과 ADL이 만든 연관 집합의 모든 요소에서 찾은 선언 집합은 다음 특별 규칙으로 병합돼요.

  1. 연관 네임스페이스의 using 지시문은 무시돼요.
  2. 연관 클래스에 선언된 네임스페이스 수준 friend 함수(및 함수 템플릿)는 일반 조회로는 안 보여도 ADL로는 보여요.
  3. 함수와 함수 템플릿을 제외한 모든 이름은 무시돼요(변수와 충돌 없음).

주의

ADL 때문에, 클래스와 같은 네임스페이스에 정의된 비멤버 함수와 비멤버 연산자는 그 클래스의 public 인터페이스 일부로 간주돼요(ADL로 찾아진다면).

ADL은 제네릭 코드에서 두 객체를 교환하는 잘 알려진 관용구의 근거예요.

using std::swap;
swap(obj1, obj2);

std::swap(obj1, obj2)를 직접 호출하면 obj1이나 obj2의 타입과 같은 네임스페이스에 정의된 사용자 정의 swap()을 고려하지 못하고, 한정 없는 swap(obj1, obj2)만 호출하면 사용자 정의 오버로드가 제공되지 않았을 때 아무것도 호출하지 못하기 때문이에요. 특히 std::iter_swap과 표준 라이브러리의 모든 알고리즘은 Swappable 타입을 다룰 때 이 방식을 사용해요.

이름 찾기 규칙 때문에 std 네임스페이스의 타입(예: std::vectorstd::pair에 대한 사용자 정의 operator>>operator+)을 대상으로 하는 연산자를 전역이나 사용자 정의 네임스페이스에 선언하는 것은 비현실적이에요(vector/pair의 요소 타입이 사용자 정의 타입이라서 ADL에 네임스페이스를 추가하는 경우가 아니라면요). 그런 연산자는 표준 라이브러리 알고리즘 같은 템플릿 인스턴스화에서 찾아지지 않을 거예요. 자세한 내용은 의존 이름(dependent names) 문서를 보면 돼요.

ADL은 클래스(class template) 안에만 통째로 정의된 friend 함수(보통 오버로드된 연산자)를, 네임스페이스 수준에서 선언된 적이 없어도 찾을 수 있어요.

template<typename T>
struct number
{
    number(int);
    friend number gcd(number x, number y) { return 0; }; // 클래스 템플릿 안 정의
};

// 일치하는 선언이 제공되지 않으면 gcd는
// (ADL을 통해서만 보이는) 이 네임스페이스의 보이지 않는 멤버
void g()
{
    number<double> a(3), b(4);
    a = gcd(a, b); // number<double>이 연관 클래스라 gcd를 찾음
                   // (전역 스코프의 네임스페이스에서 보이게 됨)
//  b = gcd(3, 4); // Error; gcd는 보이지 않음
}

다음 문맥에서는 ADL 전용 조회(즉, 연관 네임스페이스에서만 조회)가 일어나요.

  • 템플릿 인스턴스화 지점에서의 의존 이름 조회

예시

http://www.gotw.ca/gotw/030.htm의 예시를 가져온 거예요.

namespace A
{
    struct X;
    struct Y;
    
    void f(int);
    void g(X);
}

namespace B
{
    void f(int i)
    {
        f(i); // B::f 호출 (무한 재귀)
    }
    
    void g(A::X x)
    {
        g(x); // Error: B::g(일반 조회)와 A::g(인자 의존 조회) 사이에서 모호
    }
    
    void h(A::Y y)
    {
        h(y); // B::h 호출 (무한 재귀): ADL이 A 네임스페이스를 조사하지만
              // A::h를 찾지 못하므로 일반 조회의 B::h만 사용
    }
}

더 알아보기

  • 이름 찾기(name lookup), 템플릿 인자 추론(template argument deduction), 오버로드 결정(overload resolution)이 ADL과 맞물려 동작해요.
  • 스왑 관용구처럼 ADL을 활용하는 패턴은 제네릭 코드에서 자주 만나요.
  • cppreference의 ADL 원문에서 결함 보고 기록을 더 볼 수 있어요.