Two-Phase Lookup & ADL
C++ 템플릿은 이름 탐색을 두 단계로 나눠 처리합니다. 이 규칙을 모르면 “왜 컴파일이 안 되지?”라는 상황을 자주 만나게 됩니다. Two-Phase Lookup과 ADL(Argument-Dependent Lookup)을 제대로 이해하면 템플릿 라이브러리 작성 실력이 크게 향상됩니다.
Two-Phase Lookup이란
섹션 제목: “Two-Phase Lookup이란”템플릿 정의는 두 시점에 처리됩니다.
| 단계 | 시점 | 탐색 대상 |
|---|---|---|
| 1단계 | 템플릿 정의 시 | 비의존 이름(non-dependent name) |
| 2단계 | 템플릿 인스턴스화 시 | 의존 이름(dependent name) |
비의존 이름: 템플릿 파라미터에 의존하지 않는 이름
의존 이름: 템플릿 파라미터 T에 의존하는 이름 (T::value, func(T{}) 등)
template <typename T>void foo() { bar(); // 비의존 이름 → 1단계에서 탐색 (정의 시점 스코프) T::baz(); // 의존 이름 → 2단계에서 탐색 (인스턴스화 시점 스코프)}1단계 탐색: 비의존 이름
섹션 제목: “1단계 탐색: 비의존 이름”비의존 이름은 템플릿이 정의된 시점의 스코프에서만 탐색됩니다. 나중에 선언되는 함수는 보이지 않습니다.
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는 보이지 않음}이 동작은 직관에 반하는 경우가 많습니다. 비의존 이름은 정의 시점이 기준임을 반드시 기억해야 합니다.
2단계 탐색: 의존 이름과 ADL
섹션 제목: “2단계 탐색: 의존 이름과 ADL”의존 이름은 인스턴스화 시점에 탐색됩니다. 이때 **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 키워드
섹션 제목: “typename과 template 키워드”의존 이름이 타입인 경우 typename, 템플릿인 경우 template 키워드를 명시해야 합니다. 컴파일러는 1단계에서 의존 이름의 종류를 판단할 수 없기 때문입니다.
typename 필요 케이스
섹션 제목: “typename 필요 케이스”template <typename T>void foo() { // T::iterator는 타입인가, 정적 멤버 변수인가? // typename을 붙이지 않으면 컴파일러는 변수로 해석 typename T::iterator it; // 명시적으로 타입임을 선언}template 필요 케이스
섹션 제목: “template 필요 케이스”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로 해결합니다.
실전 함정 3: 전방 선언과 ODR
섹션 제목: “실전 함정 3: 전방 선언과 ODR”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이 정의되어 있어야 합니다.
Niebloid와 CPO (C++20)
섹션 제목: “Niebloid와 CPO (C++20)”C++20 std::ranges는 ADL 차단을 위해 Niebloid 패턴을 사용합니다. Niebloid는 함수 객체로 구현되어 ADL이 발동하지 않습니다.
// std::sort vs std::ranges::sortstd::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은 복잡하지만, 이 규칙을 이해하면 템플릿 라이브러리 코드를 훨씬 명확하게 읽고 디버깅할 수 있습니다.