KoreanFoodie's Study

Effective C++ | 항목 42 : typename 의 두 가지 의미를 제대로 파악하자 본문

Tutorials/C++ : Advanced

Effective C++ | 항목 42 : typename 의 두 가지 의미를 제대로 파악하자

GoldGiver 2022. 10. 25. 16:30

C++ 프로그래머의 필독서이자 바이블인, 스콧 마이어스의 Modern Effective C++ 를 읽고 기억할 내용을 요약하고 있습니다. 꼭 읽어보시길 추천드립니다!

항목 42 : typename 의 두 가지 의미를 제대로 파악하자

핵심 :

1. 템플릿 매개변수를 선언할 때, class 및 typename 은 서로 바꾸어 써도 무방하다.
2. 중첩 의존 타입 이름을 식별하는 용도에는 반드시 typename 을 사용한다. 단, 중첩 의존 이름이 기본 클래스 리스트에 있거나 멤버 초기화 리스트 내의 기본 클래스 식별자로 있는 경우에는 예외이다.


질문 : 아래의 두 템플릿 선언문에 쓰인 class 와 typename 의 차이는 무엇일까?

template<typename T> class Widget;
template<class T> class Widget;

답변 : 차이가 없음
그렇다면 typename 과 class, 두 개의 키워드는 왜 생긴걸까? 다음 예시를 통해 typename 을 써야 하는 케이스를 알아보자.

template<typename C>
// 컨테이너의 두 번째 원소를 출력하는 함수
void print2nd(const C& conatiner)
{
	if (conatiner.size() >= 2)
	{
		C::const_iterator iter(conatiner.begin());

		++iter;
		int value = *iter;
		std::cout << value;
	}
}

위의 const_iterator 처럼, 템플릿 매개변수에 종속된 것을 의존 이름(dependent name) 이라고 한다. 의존 이름이 어떤 클래스 안에 중첩되어 있는 이런 경우의 이름을 중첩 의존 이름(nested dependent name) 이라고 부른다. 위의 코드에서 C::const_iterator 는 정확히 말하면 중첩 의존 타입 이름(nested dependent type name) 이다. 반면 value 는 int 타입으로, 비의존 이름(non-dependent name) 이다.
코드 안에 중첩 의존 이름이 있으면 구문 분석 과정에서 에러가 날 수 있다. 예를 들어 다음 코드를 보자.

template<typename C>
void print2nd(const C& conatiner)
{
	C::const_iterator * x;
}

언뜻 보면 C::const_iterator 타입 포인터 x 를 선언하는 것처럼 보이지만, 만약 C 클래스에 const_iterator 라는 변수가 있으면, 위 코드는 C::const_iterator 와 x 를 피연산자로 하는 곱셈 연산이 된다!
이런 황당한 경우를 해결하려면, C::const_iterator 앞에 typename 을 붙여 주어야 한다.

template<typename C>
void print2nd(const C& conatiner)
{
	typename C::const_iterator * x;
}

typename 키워드는 중첩 의존 이름만 식별하는 데 써야 한다. 즉, print2nd 의 매개변수에 오는 const C& container 앞에는 typaname 을 붙여주면 안된다.
예외가 하나 있는데, 중첩 의존 타입 이름이 기본 클래스의 리스트에 있거나 멤버 초기화 리스트 내의 기본 클래스 식별자로서 있을 경우에는 typename 을 붙이면 안된다는 것이다.

template<typename T>
// 상속되는 기본 클래스 리스트 : typename 쓰면 안 됨
class Derived: public Base<T>::Nested
{
public:
	// 멤버 초기화 리스트에 있는 기본 클래스 식별자
	// : typename 쓰면 안 됨
	explicit Derived(int x)
	: Base<T>::Nested(x)
	{
		// 중첩 의존 타입 이름 : typename 필요
		typename Base<T>::Nested temp;
	}
};


마지막으로, 현업에서 볼 만한 대표적인 사례를 하나 보자. 반복자를 매개변수로 받아 반복자가 가리키는 객체의 사본을 temp 라는 이름의 지역 변수로 만든다고 가정하자.

template<typename IterT>
void workWithIteragot(IterT iter)
{
	typename std::iterator_traits<IterT>::value_type temp(*iter);
}

위의 typename std::iterator_traits<IterT>::value_type 은 풀어 설명하면 "IterT 타입의 객체로 가리키는 대상의 타입" 이라는 뜻이다. 즉, iter 의 타입이 vector<int>::iterator 라면, temp 는 int 타입이 될 것이다.
typename std::iterator_traits<IterT>::value_type 은 다음과 같이 간략화가 가능한데, typedef 이름을 만들 때는 그 멤버 이름과 똑같이 짓는 것이 관례이다.

template<typename IterT>
void workWithIteragot(IterT iter)
{
	typedef typename std::iterator_traits<IterT>::value_type value_type;
	value_type temp(*iter);
}

추가적으로, 컴파일러마다 typename 에 관한 규칙을 얼마나 강조하는지는 약간의 차이를 보인다!

Comments