종속 이름

종속 이름 (Dependent names)

템플릿(클래스 템플릿과 함수 템플릿 모두)의 정의 안에서는, 어떤 구성 요소의 의미가 인스턴스화마다 달라질 수 있어요. 특히 타입과 표현식이 타입 템플릿 매개변수의 타입이나 비타입 템플릿 매개변수의 값에 의존할 수 있죠. 이런 의존적인 이름을 종속 이름(dependent name) 이라고 해요. 종속 이름과 비종속 이름(non-dependent name)은 이름 검색(lookup)과 바인딩(binding)이 서로 다르게 일어나요.

출처: cppreference

본문

무슨 말인지 예부터 볼게요. 아래에서 B<T>, T::A, B<T>::i, pb->j는 모두 T에 의존하는 종속 이름이에요.

template<typename T>
struct X : B<T> // "B<T>"는 T에 의존
{
    typename T::A* pa; // "T::A"는 T에 의존
                       // (여기서 "typename"을 쓰는 뜻은 아래 참고)

    void f(B<T>* pb)
    {
        static int i = B<T>::i; // "B<T>::i"는 T에 의존
        pb->j++; // "pb->j"는 T에 의존
    }
};

바인딩 규칙 (Binding rules)

비종속 이름템플릿 정의 지점에서 검색되고 바인딩돼요. 이 바인딩은, 템플릿 인스턴스화 지점에서 더 나은 일치가 있더라도 그대로 유지돼요.

#include <iostream>

void g(double) { std::cout << "g(double)\n"; }

template<class T>
struct S
{
    void f() const
    {
        g(1); // "g"는 비종속 이름, 지금 바인딩됨
    }
};

void g(int) { std::cout << "g(int)\n"; }

int main()
{
    g(1);  // g(int) 호출

    S<int> s;
    s.f(); // g(double) 호출
}

만약 비종속 이름의 의미가 정의 문맥과 템플릿 특수화의 인스턴스화 지점 사이에서 바뀌면, 그 프로그램은 ill-formed이며 진단이 필요 없어요. 이는 다음 상황에서 가능해요.

  • 비종속 이름에 쓰인 타입이 정의 지점에서는 불완전하지만 인스턴스화 지점에서는 완전하다.
  • (C++17부터) 템플릿 정의에서의 이름 검색이 using 선언을 찾았는데, 인스턴스화의 대응하는 스코프에서의 검색은 그 using 선언이 팩 확장이었고 대응하는 팩이 비어 있어 어떤 선언도 찾지 못한다.
  • 인스턴스화가 정의 지점에서 아직 정의되지 않은 기본 인자나 기본 템플릿 인자를 사용한다.
  • 인스턴스화 지점의 상수 표현식이 정수·미한정 enum 타입(constexpr 객체·참조의 값, or since C++11 constexpr 함수의 정의)의 const 객체의 값을 사용하는데, 그 객체/참조/함수(since C++11)가 정의 지점에서 정의되지 않았다.
  • 템플릿이 인스턴스화 지점에서 비종속 클래스 템플릿 특수화나 변수 템플릿 특수화(since C++14)를 사용하는데, 그 템플릿이 정의 지점에서 정의되지 않은 부분 특수화에서 인스턴스화됐거나, 정의 지점에서 선언되지 않은 명시적 특수화를 가리킨다.

종속 이름의 바인딩은 검색(lookup)이 일어날 때까지 연기돼요.

검색 규칙 (Lookup rules)

템플릿에 쓰인 종속 이름의 검색은 템플릿 인자가 알려질 때까지 연기돼요. 그 시점에:

  • 비-ADL 검색(non-ADL lookup) 은 템플릿 정의 문맥에서 보이는 외부 결합을 가진 함수 선언들을 조사해요.
  • ADL(argument-dependent lookup) 은 템플릿 정의 문맥 또는 인스턴스화 문맥에서 보이는 외부 결합을 가진 함수 선언들을 조사해요.

다시 말해, 템플릿 정의 후에 새 함수 선언을 추가해도 (ADL을 통하지 않는 한) 보이지 않아요.

이 규칙의 목적은 템플릿 인스턴스화의 ODR 위반을 막아주는 것이에요.

// 외부 라이브러리
namespace E
{
    template<typename T>
    void writeObject(const T& t)
    {
        std::cout << "Value = " << t << '\n';
    }
}

// 번역 단위 1:
// 프로그래머 1은 E::writeObject가 vector<int>에서 동작하길 원함
namespace P1
{
    std::ostream& operator<<(std::ostream& os, const std::vector<int>& v)
    {
        for (int n : v)
            os << n << ' ';
        return os;
    }

    void doSomething()
    {
        std::vector<int> v;
        E::writeObject(v); // 오류: P1::operator<<를 찾지 못함
    }
}

// 번역 단위 2:
// 프로그래머 2는 E::writeObject가 vector<int>에서 동작하길 원함
namespace P2
{
    std::ostream& operator<<(std::ostream& os, const std::vector<int>& v)
    {
        for (int n : v)
            os << n << ':';
        return os << "[]";
    }

    void doSomethingElse()
    {
        std::vector<int> v;
        E::writeObject(v); // 오류: P2::operator<<를 찾지 못함
    }
}

만약 위 예제에서 operator<<에 대한 비-ADL 검색이 인스턴스화 문맥에서 허용됐다면, E::writeObject<vector<int>>의 인스턴스화는 두 가지 다른 정의를 갖게 돼요. 하나는 P1::operator<<를, 다른 하나는 P2::operator<<를 쓰는 정의예요. 이런 ODR 위반은 링커가 감지하지 못할 수 있고, 두 인스턴스 중 하나가 둘 다에서 쓰이는 결과를 낳을 수 있어요.

ADL이 사용자 정의 네임스페이스를 조사하게 하려면, std::vector를 사용자 정의 클래스로 바꾸거나, 그 원소 타입을 사용자 정의 클래스로 만들면 돼요.

namespace P1
{
    // C가 P1 네임스페이스에 정의된 클래스라면
    std::ostream& operator<<(std::ostream& os, const std::vector<C>& v)
    {
        for (C n : v)
            os << n;
        return os;
    }

    void doSomething()
    {
        std::vector<C> v;
        E::writeObject(v); // OK: writeObject(std::vector<P1::C>)를 인스턴스화하며
                           //     ADL로 P1::operator<<를 찾음
    }
}

참고로 이 규칙 때문에 표준 라이브러리 타입에 대한 연산자 오버로딩이 실용적이지 않아요.

#include <iostream>
#include <iterator>
#include <utility>
#include <vector>

// 나쁜 생각: 연산자가 전역 네임스페이스에 있지만 인자는 std::에 있음
std::ostream& operator<<(std::ostream& os, std::pair<int, double> p)
{
    return os << p.first << ',' << p.second;
}

int main()
{
    typedef std::pair<int, double> elem_t;
    std::vector<elem_t> v(10);
    std::cout << v[0] << '\n'; // OK, 일반 검색이 ::operator<<를 찾음
    std::copy(v.begin(), v.end(),
              std::ostream_iterator<elem_t>(std::cout, " "));
    // 오류: std::ostream_iterator의 정의 지점에서의 일반 검색과 ADL 모두
    // std 네임스페이스만 고려하고, std::operator<<의 오버로드를 많이 찾으므로
    // 검색이 수행됩니다.
    // 오버로드 해석은 검색이 찾은 집합 안에서 elem_t에 대한 operator<<를
    // 결국 찾지 못합니다.
}

참고로 종속 이름의 제한된 검색(바인딩은 아님) 도 템플릿 정의 시점에 일어나요. 이것은 종속 이름과 비종속 이름을 구별하고, 이름이 현재 인스턴스화의 멤버인지 알 수 없는 특수화의 멤버인지 정하는 데 필요해요. 이 검색으로 얻은 정보로 오류를 감지할 수도 있어요(아래 참고).

종속 타입 (Dependent types)

다음 타입들은 종속 타입(dependent type) 이에요.

  • 템플릿 매개변수.
  • 알 수 없는 특수화의 멤버(아래 참고).
  • 알 수 없는 특수화의 종속 멤버인 중첩 클래스/열거형(아래 참고).
  • 종속 타입의 cv 한정 버전.
  • 종속 타입으로 구성된 복합 타입.
  • 원소 타입이 종속적이거나 (있다면) 경계(bound)가 값-종속인 배열 타입.
  • (C++11부터) 매개변수에 함수 매개변수 팩이 하나 이상 포함된 함수 타입.
  • 예외 명세가 값-종속인 함수 타입.
  • 다음 조건 중 하나를 만족하는 템플릿 식별자(template-id):
    • 템플릿 이름이 템플릿 매개변수이다.
    • 템플릿 인자 중 하나가 타입-종속이거나 값-종속이거나 팩 확장이다 (C++11부터, 템플릿 식별자를 인자 목록 없이 주입 클래스 이름으로 써도 마찬가지).
  • (C++11부터) 타입-종속 표현식에 적용된 decltype의 결과. 타입-종속 표현식에 적용된 decltype의 결과는 유일한 종속 타입이에요. 두 결과가 같은 타입을 가리키는 건 그 표현식들이 동등(equivalent)할 때뿐이에요.
  • (C++26부터) 타입-종속 상수 표현식에 적용된 팩 인덱싱 지정자(pack indexing specifier). 타입-종속 상수 표현식에 적용된 팩 인덱싱 지정자는 유일한 종속 타입이에요. 두 팩 인덱싱 지정자가 같은 타입을 가리키는 건 그 상수 표현식들이 동등할 때뿐이에요. 그 외에는 인덱스가 같은 값일 때만 같은 타입을 가리켜요. 또한 종속 상수 표현식에 적용된 splice 타입 지정자의 결과도 종속 타입이에요.

참고로 현재 인스턴스화의 typedef 멤버는, 그것이 가리키는 타입이 종속적일 때만 종속적이에요.

타입-종속 표현식 (Type-dependent expressions)

다음 표현식들은 타입-종속(type-dependent) 이에요.

  • 어떤 부분 표현식이 타입-종속 표현식인 표현식.
  • 클래스가 종속 타입일 때의 this.
  • concept-id가 아니고(C++20부터) 다음 조건 중 하나를 만족하는 식별자 표현식:
    • 이름 검색이 적어도 하나의 종속 선언을 찾는 식별자를 포함한다.
    • 종속 템플릿 식별자(dependent template-id)를 포함한다.
    • 종속 타입으로의 변환 함수 이름을 포함한다.
    • 알 수 없는 특수화의 멤버인 중첩 이름 지정자나 한정 식별자를 포함한다.
    • "미지정 경계의 배열" 타입의 정적 데이터 멤버인 현재 인스턴스화의 종속 멤버를 가리킨다.
    • (C++11부터) 특별 식별자 __func__를 포함한다 (어떤 둘러싼 함수가 템플릿, 클래스 템플릿의 비템플릿 멤버, 또는(C++14부터) 제네릭 람다인 경우).
    • (C++14부터) 반환 타입 추론으로 선언된 현재 인스턴스화의 멤버 함수 선언을 하나 이상 찾는 식별자를 포함한다.
    • (C++17부터) 다음 중 하나를 찾는 식별자를 포함한다: 이니셜라이저가 타입-종속인 구조적 바인딩 선언, 타입에 자리표시자 auto가 포함된 비타입 템플릿 매개변수, 이니셜라이저가 타입-종속인 자리표시자 타입(예: auto 정적 데이터 멤버)이 포함된 타입으로 선언된 변수.
    • (C++26부터) 팩을 찾는 식별자, 또는 확장 문장(expansion statement) S의 item-declaration이 소개한 이름으로, 그 이름에 대해 지정된 타입에 자리표시자 타입이 포함되고 S가 반복 확장 문장이 아니거나 S의 확장 이니셜라이저가 타입-종속인 것.
  • 종속 타입으로의 어떤 캐스트 표현식.
  • 종속 타입의 객체를 만드는 new 표현식.
  • 타입이 종속적인 현재 인스턴스화의 멤버를 가리키는 멤버 접근 표현식.
  • 알 수 없는 특수화의 멤버를 가리키는 멤버 접근 표현식.
  • (C++17부터) 폴드 표현식(fold expression).
  • (C++26부터) 식별자 표현식이 타입-종속 표현식인 팩 인덱싱 표현식.

다음 표현식들은, 이 표현식들의 타입이 ~일 수 없기 때문에, 절대 타입-종속이지 않아요.

  • 리터럴.
  • 의사 소멸자 호출(pseudo-destructor call).
  • sizeof.
  • (C++11부터) sizeof..., alignof, noexcept.
  • throw.
  • typeid.
  • delete.
  • (C++20부터) requires.
  • (C++26부터) 반영 연산자 ^^.

값-종속 표현식 (Value-dependent expressions)

다음 표현식들은 값-종속(value-dependent) 이에요.

  • 상수 표현식이 요구되는 문맥에서 쓰이고, 어떤 부분 표현식이 값-종속인 표현식.
  • 다음 조건 중 하나를 만족하는 식별자 표현식:
    • 타입-종속이다.
    • 비타입 템플릿 매개변수의 이름이다.
    • 초기화되지 않은, 현재 인스턴스화의 종속 멤버인 정적 데이터 멤버를 가리킨다.
    • 현재 인스턴스화의 종속 멤버인 정적 멤버 함수를 가리킨다.
    • 값-종속 표현식에서 초기화된 정수 또는 (until C++11) 열거형 / (since C++11) 리터럴 타입의 상수이다.
    • (C++20부터) concept-id이고 그 인자 중 하나가 종속적이다.
    • (C++26부터) 확장 문장의 item-declaration이 소개한 이름이다.
  • 피연산자가 타입-종속 표현식인 다음 표현식: sizeof, typeid, (C++11부터) alignof.
  • 피연산자가 종속 type-id인 다음 표현식: sizeof, typeid.
  • 목표 타입이 종속적이거나 피연산자가 타입-종속 표현식인 다음 표현식: C 스타일 캐스트, static_cast, const_cast, reinterpret_cast, dynamic_cast, (C++11부터) 목표 타입이 종속적이거나 값-종속 표현식이 괄호나 중괄호로 감싸진 함수형 캐스트 표현식.
  • (C++11부터) 피연산자가 (C++26부터) 구조적 바인딩 팩이 아닌 sizeof... 표현식.
  • (C++17부터) 폴드 표현식.
  • 인자가 현재 인스턴스화의 종속 멤버를 가리키는 한정 식별자인 주소-취(address-of) 표현식.
  • 인자가, 핵심 상수 표현식(core constant expression)으로 평가했을 때 (C++11부터) 정적·스레드 저장 기간의 객체나 멤버 함수인 템플릿화된 엔티티를 가리키는 어떤 표현식인 주소-취 표현식.

종속 네임스페이스 (C++26부터) — 네임스페이스 이름이, 종속 splice 지정자를 포함한 네임스페이스 별칭 정의로 소개되면 종속적이에요.

template<std::meta::info R>
int fn()
{
    namespace Alias = [:R:]; // [:R:]는 종속적
    return typename Alias::T{}; // Alias는 종속적
}

namespace NS { using T = int; }
int a = fn<^^NS>();

종속 이름 (Dependent names)

이 절은 cppreference에서 아직 미완성 상태예요 (식별자 표현식 뒤에 괄호 목록이 따르는 [temp.dep]의 첫 문단이 빠져 있고, CWG issue 591 적용을 위해 다듬는 중). 말 그대로 "어떤 이름이 종속적인가"의 완전한 정리는 표준 본문을 참고해야 해요.

현재 인스턴스화 (Current instantiation)

클래스 템플릿 정의 안(그 멤버 함수와 중첩 클래스 포함)에서는 어떤 이름이 현재 인스턴스화(current instantiation) 를 가리키는 것으로 연역될 수 있어요. 이렇게 하면 어떤 오류를 인스턴스화 지점이 아니라 정의 지점에서 잡을 수 있고, 종속 이름에 대한 typename·template 구분자(disambiguator)의 요구를 없애줘요(아래 참고).

다음 이름만 현재 인스턴스화를 가리킬 수 있어요.

  • 클래스 템플릿, 클래스 템플릿의 중첩 클래스, 클래스 템플릿의 멤버, 또는 클래스 템플릿의 중첩 클래스의 멤버 정의에서: 그 클래스 템플릿 또는 중첩 클래스의 주입 클래스 이름(injected-class-name).
  • 일차(primary) 클래스 템플릿 또는 그 멤버의 정의에서: 각 인자가 그 대응하는 매개변수와 동등한(아래 정의) 일차 템플릿에 대한 템플릿 이름 뒤에 템플릿 인자 목록(또는 동등한 별칭 템플릿 특수화)이 붙은 것.
  • 클래스 템플릿의 중첩 클래스 정의에서: 현재 인스턴스화의 멤버로 쓰인 그 중첩 클래스의 이름.
  • 클래스 템플릿 부분 특수화 또는 그 멤버의 정의에서: 각 인자가 그 대응하는 매개변수와 동등한 부분 특수화에 대한 템플릿 이름 뒤에 템플릿 인자 목록이 붙은 것.
  • 템플릿화된 함수의 정의에서: 지역 클래스의 이름.

템플릿 인자가 템플릿 매개변수와 동등하면 (다음 중 하나):

  • 타입 매개변수에 대해, 템플릿 인자가 템플릿 매개변수와 같은 타입을 가리킨다.
  • 비타입 매개변수에 대해, 템플릿 인자가 그 템플릿 매개변수와 동등한 변수를 가리키는 식별자다. 변수가 템플릿 매개변수와 동등하려면:
    • 템플릿 매개변수와 같은 타입을 가진다(cv 한정은 무시).
    • 이니셜라이저가 그 템플릿 매개변수(또는 재귀적으로, 그런 변수)를 가리키는 단일 식별자로 이루어진다.
template<class T>
class A
{
    A* p1;      // A는 현재 인스턴스화
    A<T>* p2;   // A<T>는 현재 인스턴스화
    ::A<T>* p4; // ::A<T>는 현재 인스턴스화
    A<T*> p3;   // A<T*>는 현재 인스턴스화가 아님

    class B
    {
        B* p1;                 // B는 현재 인스턴스화
        A<T>::B* p2;           // A<T>::B는 현재 인스턴스화
        typename A<T*>::B* p3; // A<T*>::B는 현재 인스턴스화가 아님
    };
};

template<class T>
class A<T*>
{
    A<T*>* p1; // A<T*>는 현재 인스턴스화
    A<T>* p2;  // A<T>는 현재 인스턴스화가 아님
};

template<int I>
struct B
{
    static const int my_I = I;
    static const int my_I2 = I + 0;
    static const int my_I3 = my_I;
    static const long my_I4 = I;
    static const int my_I5 = (I);

    B<my_I>* b1;  // B<my_I>는 현재 인스턴스화:
                  //   my_I는 I와 같은 타입이고,
                  //   I만으로 초기화됨
    B<my_I2>* b2; // B<my_I2>는 현재 인스턴스화가 아님:
                  //   I + 0은 단일 식별자가 아님
    B<my_I3>* b3; // B<my_I3>는 현재 인스턴스화:
                  //   my_I3는 I와 같은 타입이고,
                  //   my_I(I와 동등)만으로 초기화됨
    B<my_I4>* b4; // B<my_I4>는 현재 인스턴스화가 아님:
                  //   my_I4의 타입(long)이 I의 타입(int)과 다름
    B<my_I5>* b5; // B<my_I5>는 현재 인스턴스화가 아님:
                  //   (I)는 단일 식별자가 아님
};

참고로 기반 클래스가 현재 인스턴스화일 수 있어요. 중첩 클래스가 그것을 둘러싼 클래스 템플릿에서 파생되는 경우예요. 종속 타입이지만 현재 인스턴스화가 아닌 기반 클래스는 종속 기반 클래스(dependent base class) 라고 해요.

template<class T>
struct A
{
    typedef int M;

    struct B
    {
        typedef void M;

        struct C;
    };
};

template<class T>
struct A<T>::B::C : A<T>
{
    M m; // OK, A<T>::M
};

이름이 현재 인스턴스화의 멤버로 분류되면 (다음 중 하나):

  • 현재 인스턴스화나 그 비종속 기반에서 미한정 검색으로 찾은 미한정 이름.
  • 정규화자(:: 왼쪽의 이름)가 현재 인스턴스화를 가리키고, 검색이 현재 인스턴스화나 그 비종속 기반에서 그 이름을 찾는 한정 이름.
  • 클래스 멤버 접근 표현식(x.yyxp->y)에 쓰인 이름으로, 객체 표현식(x 또는 *xp)이 현재 인스턴스화이고 검색이 현재 인스턴스화나 그 비종속 기반에서 그 이름을 찾는 경우.
template<class T>
class A
{
    static const int i = 5;

    int n1[i];       // i는 현재 인스턴스화의 멤버를 가리킴
    int n2[A::i];    // A::i는 현재 인스턴스화의 멤버를 가리킴
    int n3[A<T>::i]; // A<T>::i는 현재 인스턴스화의 멤버를 가리킴

    int f();
};

template<class T>
int A<T>::f()
{
    return i; // i는 현재 인스턴스화의 멤버를 가리킴
}

현재 인스턴스화의 멤버는 종속적일 수도, 비종속적일 수도 있어요.

현재 인스턴스화의 멤버 검색이 인스턴스화 지점과 정의 지점 사이에서 다른 결과를 주면, 그 검색은 모호해요. 다만 멤버 이름을 쓴다고 해서 자동으로 클래스 멤버 접근 표현식으로 변환되지는 않아요. 명시적 멤버 접근 표현식만 현재 인스턴스화의 멤버를 나타내요.

struct A { int m; };
struct B { int m; };

template<typename T>
struct C : A, T
{
    int f() { return this->m; } // 템플릿 정의 문맥에서 A::m을 찾음
    int g() { return m; }       // 템플릿 정의 문맥에서 A::m을 찾음
};

template int C<B>::f(); // 오류: A::m과 B::m 둘 다 찾음

template int C<B>::g(); // OK: 클래스 멤버 접근 문법으로의 변환은
                        // 템플릿 정의 문맥에서 일어나지 않음

알 수 없는 특수화 (Unknown specializations)

템플릿 정의 안에서 어떤 이름들은 알 수 없는 특수화(unknown specialization) 에 속하는 것으로 연역돼요. 특히 다음이 그래요.

  • :: 왼쪽에 나타나는 어떤 이름이 현재 인스턴스화가 아닌 종속 타입인 한정 이름.
  • 정규화자가 현재 인스턴스화이고, 그 이름이 현재 인스턴스화나 그 비종속 기반 클래스들 어디에도 없는데 종속 기반 클래스가 있는 한정 이름.
  • 클래스 멤버 접근 표현식(x.yyxp->y)의 멤버 이름으로, 객체 표현식(x 또는 *xp)의 타입이 종속 타입이고 현재 인스턴스화가 아닌 경우.
  • 클래스 멤버 접근 표현식의 멤버 이름으로, 객체 표현식의 타입이 현재 인스턴스화이고, 그 이름이 현재 인스턴스화나 그 비종속 기반 클래스들 어디에도 없는데 종속 기반 클래스가 있는 경우.
template<typename T>
struct Base {};

template<typename T>
struct Derived : Base<T>
{
    void f()
    {
        // Derived<T>는 현재 인스턴스화를 가리킴
        // 현재 인스턴스화에 "unknown_type"은 없음
        // 하지만 종속 기반(Base<T>)은 있음
        // 따라서 "unknown_type"은 알 수 없는 특수화의 멤버
        typename Derived<T>::unknown_type z;
    }
};

template<>
struct Base<int> // 이 특수화가 그것을 제공
{
    typedef int unknown_type;
};

이 분류 덕분에 다음 오류들을 인스턴스화 지점이 아니라 템플릿 정의 지점에서 잡을 수 있어요.

  • 어떤 템플릿 정의에, 정규화자가 현재 인스턴스화를 가리키는데 그 이름이 현재 인스턴스화의 멤버도 알 수 없는 특수화의 멤버도 아닌 한정 이름이 있다면, 그 템플릿이 결코 인스턴스화되지 않아도 프로그램은 ill-formed(진단 불필요)예요.
template<class T>
class A
{
    typedef int type;

    void f()
    {
        A<T>::type i; // OK: "type"은 현재 인스턴스화의 멤버
        typename A<T>::other j; // 오류:

        // "other"는 현재 인스턴스화의 멤버가 아님
        // 그리고 알 수 없는 특수화의 멤버도 아님
        // 왜냐하면 A<T>(현재 인스턴스화를 가리킴)에는
        // "other"가 숨을 수 있는 종속 기반이 없기 때문
    }
};
  • 어떤 템플릿 정의에, 객체 표현식이 현재 인스턴스화인 멤버 접근 표현식이 있는데 그 이름이 현재 인스턴스화의 멤버도 알 수 없는 특수화의 멤버도 아니라면, 그 템플릿이 결코 인스턴스화되지 않아도 프로그램은 ill-formed예요.

알 수 없는 특수화의 멤버는 항상 종속적이고, 다른 모든 종속 이름처럼 인스턴스화 지점에서 검색되고 바인딩돼요 (위 참고).

종속 이름에 대한 typename 구분자

템플릿(별칭 템플릿 포함)의 선언이나 정의에서, 현재 인스턴스화의 멤버가 아니고 템플릿 매개변수에 종속적인 이름은, 키워드 typename을 쓰거나 이미 타입 이름으로 확립되지 않는 한(예: typedef 선언이나 기반 클래스 이름으로 쓰임), 타입으로 간주되지 않아요.

#include <iostream>
#include <vector>

int p = 1;

template<typename T>
void foo(const std::vector<T> &v)
{
    // std::vector<T>::const_iterator는 종속 이름,
    typename std::vector<T>::const_iterator it = v.begin();

    // "typename"이 없으면 다음은 종속 데이터 멤버 "const_iterator"와
    // 어떤 변수 "p"의 곱셈으로 해석됩니다. 이 지점에 전역 "p"가 보이므로
    // 이 템플릿 정의는 컴파일됩니다.
    std::vector<T>::const_iterator* p;

    typedef typename std::vector<T>::const_iterator iter_t;
    iter_t * p2; // "iter_t"는 종속 이름이지만 타입 이름임이 알려짐
}

template<typename T>
struct S
{
    typedef int value_t; // 현재 인스턴스화의 멤버

    void f()
    {
        S<T>::value_t n{}; // S<T>는 종속적이지만 "typename"은 불필요
        std::cout << n << '\n';
    }
};

int main()
{
    std::vector<int> v;
    foo(v); // 템플릿 인스턴스화 실패: std::vector<int> 타입에
            // "const_iterator"라는 멤버 변수가 없음
    S<int>().f();
}

키워드 typename은 이런 방식으로 한정 이름 앞에만(예: T::x) 쓸 수 있지만, 이름이 종속적일 필요는 없어요.

typename이 앞에 붙은 식별자에는 보통의 한정 이름 검색이 사용돼요. 정교한 타입 지정자(elaborated type specifier)와 달리, 정규화자가 있어도 검색 규칙이 바뀌지 않아요.

struct A // A는 중첩 변수 X와 중첩 타입 struct X를 가짐
{
    struct X {};
    int X;
};

struct B
{
    struct X {}; // B는 중첩 타입 struct X를 가짐
};

template<class T>
void f(T t)
{
    typename T::X x;
}

void foo()
{
    A a;
    B b;
    f(b); // OK: f<B>를 인스턴스화, T::X는 B::X를 가리킴
    f(a); // 오류: f<A>를 인스턴스화할 수 없음:
          // A::X에 대한 한정 이름 검색이 데이터 멤버를 찾기 때문
}

키워드 typename템플릿 밖에서도 쓸 수 있어요.

#include <vector>

int main()
{
    // 둘 다 OK (CWG 382 해결 후)
    typedef typename std::vector<int>::const_iterator iter_t;
    typename std::vector<int> v;
}

(C++26부터) typename 키워드를 splice 지정자에서 써서 그것이 타입을 가리킨다는 것을 나타낼 수도 있어요.

(C++20부터) 어떤 문맥에서는 타입 이름만 유효하게 나타날 수 있어요. 그런 문맥에서는 종속 한정 이름이나 (C++26부터) splice 지정자가 타입 이름을 가리키는 것으로 가정되고 typename이 필요 없어요:

  • 다음의 (최상위) decl-specifier-seq에서 선언 지정자로 쓰인 한정 이름: 네임스페이스 스코프의 단순 선언이나 함수 정의, 클래스 멤버 선언, 클래스 멤버 선언(friend 함수 선언 포함)·기본 인자 밖의 매개변수 선언, 이름이 한정된 함수·함수 템플릿 선언자의·기본 인자 밖의 매개변수 선언, 기본 인자 밖의 람다 표현식 매개변수 선언, requires 표현식의 매개변수 선언, 비타입 템플릿 매개변수 선언의 타입.
  • type-id에 나타나는데, 가장 작게 둘러싸는 type-id가 다음 중 하나인 한정 이름: 자신의 타입을 괄호로 감싸지 않는 new 표현식의 타입, 별칭 선언의 type-id, 후행 반환 타입, 타입 템플릿 매개변수의 기본 인자, 또는 static_cast·dynamic_cast·const_cast·reinterpret_cast의 type-id.

종속 이름에 대한 template 구분자

마찬가지로, 템플릿 정의에서 현재 인스턴스화의 멤버가 아닌 종속 이름은, 구분 키워드 template을 쓰거나 이미 템플릿 이름으로 확립되지 않는 한 템플릿 이름으로 간주되지 않아요.

template<typename T>
struct S
{
    template<typename U>
    void foo() {}
};

template<typename T>
void bar()
{
    S<T> s;
    s.foo<T>();          // 오류: <가 less-than 연산자로 해석됨
    s.template foo<T>(); // OK
}

키워드 template::(스코프 해석), ->(포인터를 통한 멤버 접근), .(멤버 접근) 연산자 뒤에만 이 방식으로 쓸 수 있어요. 다음은 모두 유효한 예예요.

T :: template foo < X > ();

s . template foo < X > ();

this -> template foo < X > ();

typename T :: template iterator < int >:: value_type v;

typename과 마찬가지로, template 접두사는 이름이 종속적이지 않거나 사용처가 템플릿 스코프가 아니어도 허용돼요.

:: 왼쪽의 이름이 네임스페이스를 가리켜도 template 구분자는 허용돼요.

template<typename>
struct S {};

::template S<void> q; // 허용되지만 불필요

(until C++23) 멤버 접근 표현식의 템플릿 이름에 대한 미한정 이름 검색의 특별 규칙 때문에, 비종속 템플릿 이름이 멤버 접근 표현식(->. 뒤)에 나타나면, 그 표현식의 문맥에서 일반 검색이 같은 이름의 클래스(또는 C++11부터 별칭) 템플릿을 찾으면 구분자가 불필요해요. 하지만 표현식 문맥에서 찾은 템플릿이 클래스 문맥에서 찾은 것과 다르면 프로그램은 ill-formed예요.

template<int> struct A { int value; };

template<class T> void f(T t)
{
    t.A<0>::value; // A의 일반 검색은 클래스 템플릿을 찾음.
                   // A<0>::value는 클래스 A<0>의 멤버를 가리킴
    // t.A<0;     // 오류: "<"가 템플릿 인자 목록의 시작으로 취급됨
}

(C++26부터) template 키워드를 splice 지정자에서 써서 그것이 템플릿을 가리킨다는 것을 나타낼 수도 있어요.

키워드 (Keywords)

관련 키워드는 templatetypename이에요.

결함 보고 (Defect reports)

다음의 동작을 바꾸는 결함 보고가 이전에 공표된 C++ 표준에 소급 적용됐어요.

DR 적용 대상 공표된 동작 올바른 동작
CWG 206 C++98 비종속 이름에 쓰인 타입이 템플릿 정의 시점에는 불완전하고 인스턴스화 시점에는 완전할 때 의미 제약을 언제 적용할지 불특정이었음 이 경우 ill-formed이고 진단 불필요
CWG 224 C++98 종속 타입의 정의가 검색이 아니라 이름의 형식에 근거했음 정의 개편
CWG 382 C++98 typename 구분자가 템플릿 스코프에서만 허용됐음 템플릿 밖에서도 허용
CWG 468 C++98 template 구분자가 템플릿 스코프에서만 허용됐음 템플릿 밖에서도 허용
CWG 502 C++98 중첩 열거형이 종속적인지 불특정이었음 중첩 클래스처럼 종속적
CWG 1047 C++98 typeid 표현식이 결코 값-종속이 아니었음 피연산자가 타입-종속이면 값-종속
CWG 1160 C++98 일차 템플릿이나 부분 특수화와 일치하는 템플릿 식별자가 템플릿 멤버 정의에 나타날 때 현재 인스턴스화를 가리키는지 불특정이었음 지정됨
CWG 1413 C++98 클래스 템플릿의 초기화되지 않은 정적 데이터 멤버·정적 멤버 함수·멤버 주소가 값-종속으로 나열되지 않았음 나열됨
CWG 1471 C++98 현재 인스턴스화의 비종속 기반의 중첩 타입이 종속적이었음 종속적이지 않음
CWG 1850 C++98 정의 문맥과 인스턴스화 지점 사이에서 의미가 바뀔 수 있는 경우의 목록이 불완전했음 완전히 만듦
CWG 1929 C++98 template 구분자가 왼쪽 이름이 네임스페이스를 가리키는 :: 뒤에 올 수 있는지 불명확했음 허용
CWG 2066 C++98 this가 결코 값-종속이 아니었음 값-종속일 수 있음
CWG 2100 C++98 클래스 템플릿의 정적 데이터 멤버 주소가 값-종속으로 나열되지 않았음 나열됨
CWG 2109 C++98 타입-종속 식별자 표현식이 값-종속이 아닐 수 있었음 항상 값-종속
CWG 2276 C++98 예외 명세가 값-종속인 함수 타입이 종속 타입이 아니었음 종속 타입임
CWG 2307 C++98 템플릿 인자로 쓰인 괄호로 감싼 비타입 템플릿 매개변수가 그 템플릿 매개변수와 동등했음 더 이상 동등하지 않음
CWG 2457 C++11 함수 매개변수 팩을 가진 함수 타입이 종속 타입이 아니었음 종속 타입임
CWG 2785 C++20 requires 표현식이 타입-종속일 수 있었음 결코 타입-종속이지 않음
CWG 2905 C++11 noexcept 표현식이 피연산자가 값-종속일 때만 값-종속이었음 피연산자가 템플릿 매개변수를 포함하면 값-종속
CWG 2936 C++98 템플릿화된 함수의 지역 클래스 이름이 현재 인스턴스화의 일부가 아니었음 일부임

더 알아보기 (Learn more)

  • 템플릿 (Templates) — 템플릿 선언과 인스턴스화의 기본. 종속 이름이 등장하는 배경. (cppreference)
  • 이름 검색 (Name lookup) — 이름이 어떻게 찾아지는지의 일반 규칙. 종속 이름 검색은 그 연기된 변형이에요. (cppreference)
  • 템플릿 매개변수 (Template parameters) — 타입·비타입·템플릿 템플릿 매개변수. 종속성이 여기서 시작돼요. (cppreference)
  • typename과 template 키워드 — 종속 이름을 타입·템플릿으로 해석하도록 돕는 구분자. (cppreference)