콘텐츠로 이동

Two-Phase Lookup & ADL

C++ 템플릿은 이름 탐색을 두 단계로 나눠 처리합니다. 이 규칙을 모르면 “왜 컴파일이 안 되지?”라는 상황을 자주 만나게 됩니다. Two-Phase Lookup과 ADL(Argument-Dependent Lookup)을 제대로 이해하면 템플릿 라이브러리 작성 실력이 크게 향상됩니다.


템플릿 정의는 두 시점에 처리됩니다.

단계시점탐색 대상
1단계템플릿 정의비의존 이름(non-dependent name)
2단계템플릿 인스턴스화의존 이름(dependent name)

비의존 이름: 템플릿 파라미터에 의존하지 않는 이름
의존 이름: 템플릿 파라미터 T에 의존하는 이름 (T::value, func(T{}) 등)

template <typename T>
void foo() {
bar(); // 비의존 이름 → 1단계에서 탐색 (정의 시점 스코프)
T::baz(); // 의존 이름 → 2단계에서 탐색 (인스턴스화 시점 스코프)
}

비의존 이름은 템플릿이 정의된 시점의 스코프에서만 탐색됩니다. 나중에 선언되는 함수는 보이지 않습니다.

void helper(int) { /* A */ }
template <typename T>
void process() {
helper(42); // 1단계에서 helper(int) 탐색 → A 호출
}
void helper(double) { /* B */ } // 나중에 추가
int main() {
process<int>(); // 여전히 A를 호출 — B는 보이지 않음
}

이 동작은 직관에 반하는 경우가 많습니다. 비의존 이름은 정의 시점이 기준임을 반드시 기억해야 합니다.


의존 이름은 인스턴스화 시점에 탐색됩니다. 이때 **ADL(Argument-Dependent Lookup)**이 함께 동작합니다.

ADL은 함수 인수의 타입이 속한 네임스페이스에서도 함수를 탐색하는 규칙입니다.

namespace Lib {
struct Point { int x, y; };
void print(Point p) {
std::cout << p.x << ", " << p.y << "\n";
}
}
template <typename T>
void display(T val) {
print(val); // 의존 이름 — 인스턴스화 시 Lib 네임스페이스도 탐색
}
int main() {
Lib::Point p{1, 2};
display(p); // ADL에 의해 Lib::print 발견 → 정상 동작
}

ADL이 없다면 Lib::print를 직접 명시해야 합니다. std::swap, std::begin 같은 커스터마이제이션 포인트가 ADL에 의존합니다.


의존 이름이 타입인 경우 typename, 템플릿인 경우 template 키워드를 명시해야 합니다. 컴파일러는 1단계에서 의존 이름의 종류를 판단할 수 없기 때문입니다.

template <typename T>
void foo() {
// T::iterator는 타입인가, 정적 멤버 변수인가?
// typename을 붙이지 않으면 컴파일러는 변수로 해석
typename T::iterator it; // 명시적으로 타입임을 선언
}
template <typename T>
void bar(T obj) {
// obj.get<int>() 인지, (obj.get < int) > () 인지 모호
obj.template get<int>(); // 명시적으로 템플릿 멤버 함수 호출
}

실전 함정 1: 기반 클래스 멤버 탐색

섹션 제목: “실전 함정 1: 기반 클래스 멤버 탐색”
template <typename T>
struct Base {
void helper() { std::cout << "Base::helper\n"; }
};
template <typename T>
struct Derived : Base<T> {
void foo() {
helper(); // 컴파일 오류! 의존 이름이 아니라 1단계 탐색
this->helper(); // OK — this-> 로 의존 이름으로 만들기
Base<T>::helper(); // OK — 명시적 한정
}
};

Derived<T>의 기반 클래스 Base<T>T에 의존하므로, helper()는 1단계에서 보이지 않습니다. this->helper() 또는 Base<T>::helper()로 의존 이름임을 명시해야 합니다.


실전 함정 2: ADL 의도치 않은 함수 탐색

섹션 제목: “실전 함정 2: ADL 의도치 않은 함수 탐색”
namespace Evil {
struct Trap {};
template <typename T>
void swap(T& a, T& b) {
// 의도적으로 std::swap을 가로채는 버전
std::cout << "Evil::swap called!\n";
}
}
template <typename T>
void myAlgo(T& a, T& b) {
using std::swap;
swap(a, b); // T가 Evil::Trap이면 Evil::swap이 호출됨
}

using std::swap 이후에도 ADL이 우선 탐색하므로 Evil::swap이 호출될 수 있습니다. 이것이 커스터마이제이션 포인트 설계가 까다로운 이유입니다. C++20의 std::ranges 알고리즘은 이 문제를 Niebloid로 해결합니다.


header.h
template <typename T>
void process(T val);
// impl.cpp
#include "header.h"
void helper_impl(int) {} // 이 함수는 1단계 탐색에서 보이지 않음
template <typename T>
void process(T val) {
helper_impl(42); // 비의존 이름 — 정의 시점 기준으로 탐색
}
template void process<int>(int); // 명시적 인스턴스화

명시적 인스턴스화 전에 helper_impl이 정의되어 있어야 합니다.


C++20 std::ranges는 ADL 차단을 위해 Niebloid 패턴을 사용합니다. Niebloid는 함수 객체로 구현되어 ADL이 발동하지 않습니다.

// std::sort vs std::ranges::sort
std::sort(v.begin(), v.end()); // ADL 발동 가능
std::ranges::sort(v); // Niebloid — ADL 발동 안 함
// 사용자 정의 Niebloid 예시
namespace mylib {
inline constexpr struct _Sort_fn {
template <std::ranges::random_access_range R>
void operator()(R&& r) const {
std::sort(std::ranges::begin(r), std::ranges::end(r));
}
} sort{};
}

상황해결책
기반 클래스 멤버 접근 안 됨this->member() 또는 Base<T>::member()
의존 이름이 타입typename T::type
의존 이름이 템플릿obj.template method<T>()
ADL 제어 필요using std::swap; swap(a, b) 또는 Niebloid
비의존 이름 순서 문제정의 전에 선언 배치

Two-Phase Lookup은 복잡하지만, 이 규칙을 이해하면 템플릿 라이브러리 코드를 훨씬 명확하게 읽고 디버깅할 수 있습니다.