한글 번역
title: “Tip of the Week #49: Argument-Dependent Lookup”
layout: tips
sidenav: side-nav-tips.html
published: true
permalink: tips/49
type: markdown
order: “049”
—
원래 게시 날짜: 2013-07-14, totw/49
“…어떤 법적 용어의 흔적을 따르든 간에 사라져버린 그것…”
– 안토닌 스칼리아, 미국 대 Windsor 반대 의견
개요
C++에서 func(a, b, c)
처럼 함수 이름 앞에 ::
범위 연산자를 사용하지 않은 함수 호출 표현은 비한정(unqualified) 호출이라고 합니다. 비한정 함수 호출 시, 컴파일러는 호출에 해당하는 함수 선언을 찾기 위해 특정 검색 과정을 수행합니다. 다른 언어와 다르게 놀라운 점은 호출자의 어휘적 범위 외에도 함수 인자 타입과 관련된 네임스페이스가 검색 범위에 추가된다는 것입니다. 이를 인자 기반 탐색(Argument-Dependent Lookup, ADL)이라고 합니다. ADL은 코드에서 흔히 발생하기 때문에 이를 기본적으로 이해하는 것이 중요합니다.
이름 탐색 기본 사항
함수 호출은 컴파일러에 의해 단일 함수 정의로 매핑되어야 합니다. 이는 두 단계로 나뉩니다:
-
이름 탐색(name lookup)
함수 이름에 해당하는 오버로드 집합을 찾기 위해 범위를 검색하는 과정입니다. -
오버로드 해결(overload resolution)
이름 탐색 결과에 따라 가장 적합한 오버로드를 선택하는 과정입니다.
이 두 과정은 독립적이며 순차적으로 처리됩니다. 이름 탐색은 호출에 적합한 함수인지 판단하지 않고 단순히 함수 이름을 검색합니다. 이후 오버로드 해결에서 인자 수와 타입을 고려하여 최적의 함수를 결정합니다.
비한정 함수 호출이 발생하면 함수 이름과 일치하는 오버로드를 찾기 위해 여러 독립적인 탐색 시퀀스가 시작됩니다. 가장 명확한 탐색은 호출 위치의 어휘적 범위에서 시작하는 외부 탐색입니다.
예시:
namespace b {
void func();
namespace internal {
void test() { func(); } // ok: b::func()를 찾음.
} // namespace internal
} // namespace b
위 코드에서 func()
는 ADL과 관련이 없습니다(인자가 없기 때문). 단순히 함수 호출 위치의 범위에서 바깥으로 검색이 진행됩니다. 이 과정은 아래와 같이 진행됩니다:
- 로컬 함수 범위.
- 클래스 범위.
- 포함된 클래스 및 상위 클래스 범위.
- 네임스페이스 범위.
- 전역 네임스페이스(
::
).
범위를 검색하다가 함수 이름이 있는 범위를 찾으면 해당 범위에서의 오버로드만 이름 탐색 결과에 포함됩니다.
중요한 원칙:
이름 탐색은 “적합한 인자”를 고려하지 않습니다. 예를 들어:
namespace b {
void func(const string&); // b::func
namespace internal {
void func(int); // b::internal::func
namespace deep {
void test() {
string s("hello");
func(s); // 오류: b::internal::func(int)만 찾음.
}
} // namespace deep
} // namespace internal
} // namespace b
위 코드는 b::internal::func(int)
만 탐색하고 b::func(const string&)
에는 도달하지 못합니다. 이름 탐색은 범위에서 일치하는 이름을 찾으면 멈추기 때문에, “명백히 잘못된” 매칭인지 여부는 오버로드 해결 단계에서 판단합니다.
인자 기반 탐색(ADL)
인자가 있는 함수 호출 시, 함수 호출의 각 인자와 연관된 네임스페이스를 추가로 탐색합니다. 이러한 ADL 탐색은 어휘적 범위 탐색과는 달리 상위 네임스페이스로 확장되지 않습니다.
어휘적 범위 탐색과 ADL 결과는 병합되어 최종적으로 오버로드 해결 과정에서 사용됩니다.
간단한 예시:
namespace aspace {
struct A {};
void func(const A&); // ADL 탐색으로 발견
} // namespace aspace
namespace bspace {
void func(int); // 어휘적 범위 탐색으로 발견
void test() {
aspace::A a;
func(a); // aspace::func(const aspace::A&)
}
} // namespace bspace
func(a)
호출은 두 개의 탐색을 시작합니다:
- 어휘적 범위 탐색
bspace
범위에서func(int)
발견. - ADL 탐색
aspace
네임스페이스에서func(const A&)
발견.
오버로드 해결에서는 aspace::func(const A&)
가 가장 적합한 매칭으로 선택됩니다.
타입-연관 네임스페이스
ADL 탐색 시, 인자의 타입에 연관된 모든 네임스페이스를 검색합니다. 이때 타입 이름에 포함된 템플릿 매개변수와 상속 관계도 포함됩니다. 예를 들어:
namespace aspace {
struct A {};
template <typename T> struct AGeneric {};
void func(const A&);
template <typename T> void find_me(const T&);
} // namespace aspace
namespace bspace {
struct B : aspace::A {};
template <typename T> struct BGeneric {};
void test() {
func(B()); // aspace 네임스페이스 검색
find_me(BGeneric<aspace::A>()); // aspace 검색
find_me(aspace::AGeneric<int>()); // aspace 검색
}
} // namespace bspace
팁과 주의사항
1. 타입 별칭
typedef
나 using
선언으로 인해 타입 연관 네임스페이스가 헷갈릴 수 있습니다.
namespace cspace {
void test() {
func(bspace::AliasForA()); // bspace가 아닌 aspace에서 검색
}
} // namespace cspace
2. 이터레이터 주의
이터레이터의 네임스페이스 연관성을 알기 어렵기 때문에 ADL에 의존하지 마십시오.
std::vector<int> vec(a);
// 컴파일 여부가 플랫폼마다 다를 수 있음!
return count(vec.begin(), vec.end(), 0);
이터레이터가 단순 포인터인지, 다른 네임스페이스 타입인지 불분명할 수 있습니다.
3. 연산자 오버로드
operator+
, operator<<
등은 비한정 함수 호출로 취급됩니다. 반드시 연관 타입과 같은 네임스페이스에 정의해야 합니다.
마무리
ADL은 C++에서 강력한 도구이지만 예상치 못한 결과를 초래할 수 있습니다. 위 내용을 숙지하면 코드에서 함수 호출과 연산자가 어떤 기준으로 해석되는지 더 명확히 이해할 수 있을 것입니다.