# C++ 컴파일러가 이름의 엉킴을 어떻게 푸는가: ADL, 템플릿, 그리고 2단계 검색
이름 검색은 C++ 컴파일의 가장 복잡한 단계 중 하나입니다. 언어의 역사적 진화로 인해 검색 규칙이 꼬여서 경험이 풍부한 개발자조차 예상치 못한 오류를 겪게 됩니다. 검색 메커니즘이 어떻게 작동하는지, 그리고 왜 컴파일러마다 다르게 동작하는지 하나씩 풀어보겠습니다.
주요 용어: 컴파일러가 이름을 분류하는 방법
컴파일러는 코드의 이름을 분석할 때 정확한 용어를 사용합니다. 이 용어들을 이해하는 것이 검색의 '마법'을 푸는 열쇠입니다.
비자격 식별자 (unqualified-id) — 이름의 기본 형태로, 예를 들어 speed, foo, 또는 my_var입니다. 여기에 연산자, 소멸자, 사용자 정의 리터럴도 포함됩니다. 예시:
// Nekvalifitsirovannye names
vector<int> v; // 'vector' — unqualified name
sort(v.begin(), v.end()); // 'sort' — unqualified name
자격 식별자 (qualified-id) — 스코프 해석 연산자 ::가 붙은 이름으로, 예를 들어 std::vector 또는 Foo::bar입니다. 여기서 Foo는 한정자(qualifier) 역할을 하고, bar는 최종 이름(terminal name)입니다.
// Kvalifitsirovannye names
std::vector<int> v; // 'vector' — qualified name
Foo::bar(); // 'bar' — terminal qualified name
템플릿-id — 명시적 인수가 붙은 템플릿 이름으로, 예를 들어 std::vector<int>입니다.
// Template-id
std::vector<int> v1; // 'vector<int>' — template-id
std::pair<int, float> p; // 'pair<int, float>' — template-id
최종 이름 (terminal name) — 컴파일러가 실제로 검색하는 이름 체인의 마지막 이름입니다. 예를 들어 obj->f()에서 최종 이름은 f이고, ns::Foo::bar()에서는 bar입니다. 템플릿에서 특히 중요합니다:
template<typename T>
void wrapper(T& obj) {
obj.size(); // 'size' — terminal name
T::value_type x; // 'value_type' — terminal qualified name
}
ADL: 인수가 주도권을 쥘 때
인수 의존적 검색(Argument-Dependent Lookup, ADL)은 인수 타입에 따라 함수 검색 범위를 자동으로 확장하는 메커니즘입니다. ADL이 없으면 사용자 정의 타입에 대한 오버로드된 연산자를 사용하기가 번거로웠을 겁니다.
다음 예를 보세요:
namespace sak {
struct BigNum { int value; };
std::ostream& operator<<(std::ostream& os, const BigNum& n) {
return os << "BigNum(" << n.value << ")";
}
}
int main() {
sak::BigNum n(42);
std::cout << n; // ADL nakhodit operator<< in sak
}
컴파일러는 n의 타입이 sak::BigNum임을 보고 operator<< 검색 범위에 sak 네임스페이스를 추가합니다. ADL이 없으면 sak::operator<<(std::cout, n)처럼 써야 합니다.
ADL은 1995년 Andrew Koenig이 제안(N0645 문서)했으나, AT&T Bell Labs에서 이미 사용되던 아이디어였습니다. 표준에서는 [basic.lookup.koenig]에 명문화했습니다. 하지만 ADL의 함정도 있습니다: 두 라이브러리가 서로 다른 네임스페이스에 같은 이름의 함수를 정의하면 충돌이 생길 수 있습니다.
ADL 알고리즘:
- 일반적인 비자격 검색을 수행합니다.
- 함수 호출 시 ADL이 작동: 인수 타입의 네임스페이스를 검색에 추가합니다.
- 이름이 자격화된 경우(예:
std::sort)에는 ADL을 적용하지 않습니다.
템플릿의 2단계 검색: GCC와 Clang이 왜 다를까
템플릿에서 이름 검색은 두 단계로 나뉩니다:
- 1단계 (템플릿 정의 시): 템플릿 매개변수와 무관한 비의존 이름(non-dependent names)을 검색합니다.
- 2단계 (인스턴스화 시): 템플릿 매개변수에 의존하는 의존 이름(dependent names)을 검색하며, 여기서 ADL이 적용됩니다.
역사적으로 컴파일러들이 이 규칙을 다르게 해석했습니다. GCC 7 이전 버전은 '지연 파싱(lazy parsing)'을 사용해 템플릿 본문을 정의 시 분석하지 않고, 인스턴스화 시 모든 이름을 검색했습니다. 이는 표준 비준수였습니다.
차이점이 드러나는 예시:
void foo(int) {} // globalnaya foo
template<typename T>
void bar(T x) {
foo(x); // zavisimoe name — ischetsya in phase 2 (ADL), vsyo correct
foo(42); // NEzavisimoe name — must iskatsya in phase 1
}
namespace myns {
struct MyType {};
void foo(MyType) {}
}
bar(myns::MyType{}); // GCC: kompiliruetsya, Clang: oshibka
Clang에서는 foo(42)가 1단계에서 검색되어 MyType에 맞는 오버로드를 찾지 못해(전역 foo(int)가 맞지 않음) 오류가 발생합니다. GCC는 인스턴스화까지 검색을 미루고 foo(int)를 찾습니다.
표준 위원회는 1단계에서 의존 베이스 클래스 검색을 금지했습니다. 다음 예를 보세요:
template<typename T>
struct Base {};
template<>
struct Base<int> {
void process() {}
};
template<typename T>
struct Derived : Base<T> {
void run() {
process(); // Error: poszukiwanie in zavisimoy baze zapreschyon in phase 1
}
};
1단계에서 Base<T> 검색이 허용되면 T=int일 때는 컴파일되지만 T=float일 때는 안 될 겁니다. 이는 템플릿의 예측 가능성을 깨뜨립니다.
핵심 포인트: 기억할 사항
C++ 이름 검색 작업 시 다음을 유의하세요:
- 최종 이름이 컴파일러의 초점입니다. 이를 파악하면 어디서 검색하는지 알 수 있습니다.
- ADL은 인수 네임스페이스까지 검색 범위를 넓힙니다. 연산자에 좋지만 충돌을 일으킬 수 있습니다.
- 템플릿에서 비의존 이름은 정의 시, 의존 이름은 인스턴스화 시 검색됩니다. 의존 타입에는
typename을 사용하세요. - 컴파일러마다 엣지 케이스를 다르게 처리할 수 있습니다. GCC, Clang, MSVC에서 테스트하세요.
- 의존 베이스 클래스에서는
this->나 스코프를 명시적으로 사용해 이름을 한정하세요.
— Editorial Team
아직 댓글이 없습니다.