- 템플릿 매개변수를 선언할 때, class 및 typename은 서로 바꾸어 써도 무방하다.
- 중첩 의존 타입 이름을 식별하는 용도에는 반드시 typename을 사용한다. 단, 중첩 의존 이름이 기본 클래스 리스트에 있거나 멤버 초기화 리스트 내의 기본 클래스 식별자로 있는 경우에는 예외이다.
typename의 두 가지 의미
class와 typename은 차이가 없다.
template<class T> class Widget;
template<typename T> class Widget;
템플릿의 타입 매개변수를 선언할 때는 class와 typename의 뜻이 완전히 똑같다.
하지만 항상 class와 typename이 똑같지 않다. typename을 무조건 사용해야 할 때가 있다.
이 경우가 언제인지를 알아보려면 템플릿 안에서 참조할 수 있는 이름의 종류가 두 가지라는 것부터 알아야 한다.
템플릿 안에서의 이름 종류
함수 템플릿이 하나 있다고 가정해 보자.
이 템플릿은 STL과 호환되는 컨테이너를 받아들이도록 만들어졌고, 이 컨테이너에 담기는 객체는 int에 대입할 수 있다.
이 템플릿이 하는 일은 컨테이너에 담긴 원소들 중 두 번째 것의 값을 출력하는 것이다.
template<typename C>
void print2nd(const C& container)
{
if(container.size() >= 2)
{
C::const_iterator iter(container.begin());
++iter;
int value = *iter;
std::cout << value;
}
}
iter의 타입은 C::const_iterator이며, 템플릿 매개변 수인 C에 따라 달라진다.
템플릿 내의 이름 중에 이렇게 템플릿 매개변수에 종속된 것을 가리켜 의존 이름(dependent name)이라고 한다.
의존 이름이 어떤 클래스 안에 중첩되어 있는 경우가 있는데, 이러한 경우를 중첩 의존 이름(nested dependent name)이라고 부른다.
위의 코드에서 C::const_iterator는 중첩 의존 이름이다. 정확히는 중첩 의존 타입 이름이라고 해야 한다.
print2nd 함수에서 쓰이는 또 하나의 지역 변수, value는 int 타입이다. int는 템플릿 매개변수가 어떻든 상관없는 타입 이름이다. 이러한 이름은 비의존 이름(non-dependent name)이라고 한다.
코드 안에 중첩 의존 이름이 있으면 문제가 생길 수 있다.
template<typename C>
void print2nd(const C& container)
{
C::const_iterator* x;
...
}
C::const_iterator에 대한 포인터인 지역 변수로서 x를 선언하고 있는 것 같다.
만약 const_iterator라는 이름을 가진 정적 데이터 멤버가 C에 들어 있고 x가 다른 전역 변수의 이름이라면 위의 코드는 지역 변수를 선언한 것이 아니다.
C::const_iterator와 x를 피연산자로 한 곱셈 연산을 하게 되는 것이다.
C가 무엇인지 다른 곳에서 알려 주지 않으면, C::const_iterator가 타입인지 아닌지 컴파일러가 알 수 없다.
print2nd 함수 템플릿이 구문분석기에 의해 처리되는 순간에도 C의 정체는 저절로 밝혀지지 않는다.
이때 C++는 모호성을 해결하기 위해 구문 분석기는 템플릿 안에서 중첩 의존 이름을 만나면 타입이라고 명시되지 않는 한 그 이름이 타입이 아니라고 가정한다.
즉, 중첩 의존 이름은 기본적으로 타입이 아닌 것으로 해석된다.
template<typename C>
void print2nd(const C& container)
{
if(container.size() >= 2)
{
C::const_iterator iter(container.begin()); // 타입이 아닌 것으로 가정
...
iter의 선언이 의미가 있으려면 C::const_iterator가 반드시 타입이어야 하는데, 우리는 C++ 컴파일러에게 타입이라고 알려 주지 않았으니, 타입이 아닌 것으로 가정해 버린다.
이러한 문제를 해결하려면 C::const_iterator가 타입이라고 말해 주어야 한다.
바로 이 경우에 C::const_iterator 앞에다가 typename이라는 키워드를 붙여 놓는다.
template<typename C>
void print2nd(const C& container)
{
if(container.size() >= 2)
{
typename C::const_iterator iter(container.begin());
...
}
}
템플릿 안에서 중첩 의존 이름을 참조할 경우에는, 그 이름 앞에 typename 키워드를 붙여 주는 것을 잊지 말아야 한다.
그리고 typename 키워드는 중첩 의존 이름을 식별할 때 만 사용해야 한다. 그 외의 이름은 typename을 가져선 안 된다는 것이다.
예를 들어 어떤 컨테이너와 그 컨테이너 내의 반복자를 한꺼번에 받아들이는 함수 템플릿을 다음과 같이 만들었다고 가정해 보자.
template<typename C> // typename, class 사용 가능
void f(const C& container, // typename 사용 불가
typename C::iterator iter); // typename 필수
C는 중첩 의존 타입 이름이 아니기 때문에, 컨테이너를 선언할 때는 typename을 이 앞에 붙이면 안 된다.
반면, C::iterator는 분명히 중첩 의존 이름이기 때문에, 이 앞에는 typename이 반드시 붙여야 한다.
typename 규칙 예외
typename은 중첩 의존 타입 이름 앞에 붙여야 한다는 규칙에는 예외가 있다.
중첩 의존 타입 이름이 기본 클래스의 리스트에 있거나 멤버 초기화 리스트 내의 기본 클래스 식별자로서 있을 경우에는 typename을 붙여 주면 안 된다.
template<typename T>
class Derived: public Base<T>::Nested{ // 기본 클래스 리스트: typename 사용 불가
public:
explicit Derived(int x): Base<T>::Nested(x) // 멤버 초기화 리스트: typename 사용 불가
{
typename Base<T>::Nested temp; // 중첩 의존 타입 이름, typename 필요
...
}
...
};
typename 다른 사례
반복자를 매개변수로 받는 어떤 함수 템플릿을 만들고 있는데, 매개변수로 넘어온 반복자가 가리키는 객체의 사본을 temp라는 이름의 지역 변수로 만들어 놓고 싶다고 가정해 보자.
template<typename IterT>
void workWithIterator(IterT iter)
{
typename std::iterator_traits<IterT>::value_type temp(*iter);
...
}
(std::iterator_traits<IterT>::value_type은 C++ 표준의 특성정보 클래스를 사용한 것이다. IterT 타입의 객체로 가리키는 대상의 타입이라는 뜻이다.)
IterT 객체가 가리키는 것과 똑같은 타입의 지역 변수를 선언한 후, iter가 가리키는 객체로 그 temp를 초기화하는 문장이다.
만약 IterT가 vector<int>::iterator라면 temp의 타입은 int이다. IterT가 list<string>::iterator라면 temp의 타입은 string이다.
여기서 std::iterator_traits<IterT>::value_type은 중첩 의존 타입 이름이므로 이름 앞에 typename을 써 주어야 한다.
std::iterator_traits<IterT>::value_type이 너무 길고 사용하기 어려워 typedef를 만들고 싶을 수 있다.
typedef 이름을 만들 때는 그 멤버 이름과 똑같이 짓는 것이 관례로 되어 있다.
template<typename IterT>
void workWithIterator(IterT iter)
{
typedef typename std::iterator_traits<Iter>::value_type value_type;
value_type temp(*iter);
...
}
typedef typename이 어색할 수 있지만 문제가 없다.
사실 typename에 관한 규칙은 컴파일러마다 조금씩 차이가 있다
typename과 중첩 의존 타입 이름 사이에는 아직도 정확한 법칙이 없기에 프로그램을 이식할 때 문제가 생길 수 있다.