본문 바로가기

Effective C++/7. 템플릿과 일반화 프로그래밍

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

질문: 아래의 템플릿 선언문에 쓰인 class typename 차이점이 뭘까요?

 

 

답변 : 차이가 없다! 템플릿의 타입 매개변수를 선언할 때는 class typename 완전히 똑같다.

 

하지만 언제까지나 class typename 똑같으면 둘을 구분할 이유가 없다. 둘이 다를 때가 언제인지 알아볼려면, 일단 템플릿 안에서 우리가 참조할 있는 이름의 종류가 2가지라는 것부터 알아야 한다.

 

함수 템플릿이 하나 있다고 가정하자. 템플릿은 STL 호환되는 컨테이너를 받아드리도록 만들었고, 컨테이너에 담기는 객체는 int에 대입할 있다. 템플릿이 하는 일은 컨테이너에 담긴 원소들 두번째 것의 값을 출력하는 것이다.

 

 

여기서 지역변수 iter value 강조 표시를 한다.

 

iter 타입은 C::const_iterator인데, 이는 템플릿 매개변수 C 따라 달라지는 타입이다. 템플릿 내의 이름 중에 이렇게 템플릿 매개변수에 종속된 것을 의존 이름(dependent name) 이라고 한다. 의존 이름은 어떤 클래스 안에 중첩되어있는 경우가 많은데(C::const_iterator C 클래스 안에 있는 이름이다), 이를 필자는 중첩 의존 이름(nested dependent name)이라고 부른다. 코드의 C::const_iterator 중첩 의존 이름이다. 정확히 말하면, 중첩 의존 타입 이름(nested dependent type name)이다. 타입을 참조하는 중첩 의존 이름이란 뜻이다.

 

value 타입은 int이다. int 템플릿 매개변수가 어떻든 상관 없는 타입 이름이다. 이를 비의존 이름(non-dependent name) 이라고 한다.

 


 

코드 안에 중첩 의존 이름이 있으면 문제가 발생할 있다. 바로 컴파일러가 구문분석을 때이다.  예를 들어, print2nd() 이런 식으로 시작했다고 보자.

 

 

언뜻 보면, 그냥 C::const_iterator 대한 포인터 변수 x 선언하고 있는거 같은데, 우리가 보는 눈하고 컴파일러가 보는 눈은 다르다.

 

만약 우연히 const_iterator라는 이름의 정적 데이터 멤버가 C 안에 들어 있었다고 가정하면, 위의 코드는 x라는 변수를 선언한 것이 아니라 const_iterator x 피연산자로 하는 곱셈 연산이 되어버린다. C::const_iterator 타입이라는 확신을 컴파일러는 하지 못한다.

 

C 정체가 뭔지 다른 곳에서 알려주지 않으면, C::const_iterator 진짜 타입인지 아닌지 밝혀낼 있는 컴파일러는 없다. 템플릿이 구문분석기에 의해 처리되는 순간에도 C 정체는 저절로 밝혀지지 않는다. 이때, C++ 이런 모호성을 해결하기 위해 어떤 규칙을 사용한다. 규칙에 의하면, 구문분석기는 템플릿 안에서 중첩 의존 이름을 만나면 프로그래머가 타입이라고 알려 주지 않는 이름이 타입이 아니라고 가정하게 되어 있다. 

 

, 중첩 의존 이름은 기본적으로 타입이 아닌 것으로 해석된다.(예외가 하나 있지만, 나중에 살펴보자.)

 

 

 

이제 코드가 제정신으로 작성한 코드가 아닌 것으로 보일 것이다. iter 선언으로서 의미가 있으려면 C::const_iterator 반드시 타입이여야 하는데, 우리가 컴파일러에게 이것이 타입이라고 알려주지 않는 , 컴파일러는 제멋대로 이것이 타입이 아닌 것으로 가정한다.

 

이것을 해결할려면 컴파일러에게 타입이라고 알려줘야 하는 방법밖에 없다. C::const_iterator typename 키워드 놓으면 된다.

 

 

 

여기서 주의할

  • typename 키워드는 중첩 의존 이름만 식별하는데 써야 한다!

 

예를 들어, 어떤 컨테이너와 컨테이너 내의 반복자를 한꺼번에 받아드리는 함수 템플릿을 다음과 같이 만들었을 ,

 

 

C 중첩 의존 이름이 아니기 때문에 typename 사용하면 안되고, C::iterator 중첩 의존 이름이기 때문에 typename 써야 한다.


 

"typename 중첩 의존 타입 앞에 붙여 주어야 한다"  규칙에 예외가 하나 있다고 말했는데, 예외란

  1. 중첩 의존 타입 이름이 기본 클래스의 리스트에 있거나
  2. 맴버 초기화 리스트 내의 기본 클래스 식별자로서 있을 경우

typename 붙여주면 안된다는 것이다.

 

다음 예제를 보자.

 

대충 밑의 예제가 컴파일되게 맞춘  Base 템플릿 클래스

 

왠만한 중첩 의존 타입은 typename 붙어야 하지만, 기본 클래스 식별자에는 typename 붙여주면 안된다.

 

마지막으로 예제를 하나 보자. 예제는 현업 코드에서 쉽게 있는 형태이다. 우리가 반복자를 매개변수로 받는 어떤 함수 템플릿을 만들고 있는데, 매개변수로 넘어온 반복자가 가리키는 객체의 사본을 temp라는 이름의 지역 변수로 만들어 놓고 싶다고 가정하자. 그러면 다음과 같은 코드가 나올 것이다.

 

 

std::iterator_traits<IterT>::value_type 그저 C++ 표준 특성정보(traits) 뿐이니 걱정말자.(항목 47 참고) 코드를 우리말로 풀면 대충 "IterT 타입의 객체로 가리키는 대상의 타입"이란 뜻이다. 문장은 IterT 객체가 가리키는 것과 똑같은 타입의 지역 변수를 선언한 iter 가리키는 객체로 temp 초기화하는 문장이다. 만약 IterT vector<int>::iterator라면 temp 타입은 int 된다.

 

어쨌든, std::iterator_traits<IterT>::value_type 중첩 의존 타입 이름(value_type std::iterator_traits<IterT> 안에 중첩되어 있다.)이므로 이름 앞에는 typename 써줘야 한다.

 

근데 프로그래머 입장에서 타입을 계속 치고 있진 않을 것이다. 타입 이름이 너무 기니까 typedef 통해 이름을 바꿔주고 싶다. 참고로 특성정보 클래스에 속한 value_type 등의 맴버 이름(항목 47 참고) 대해 typedef 이름을 만들 때는 맴버 이름과 똑같이 짓는 것이 관례이다. 따라서 보통 이렇게 한다.

 

 

여기서 typedef typename 같이 있는게 뭔가 이상해 보이지만, 이때의 typename 그저 중첩 의존 타입 이름을 참조하는데 지켜야할 규칙 때문에 생긴 것이기 때문에 논리적으로 하자가 없다. 그래서 이렇게 써도 되고 왠만하면 이렇게 타입은 typedef 줄여서 쓰자.

 

이야기를 끝내기 전에 하나만 말하자면, 사실 이번 항복에 나온 typename 관한 규칙을 얼마나 강조하느냐는 컴파일러마다 조금씩 차이가 있다. 어떤 컴파일러는 typename 써야 하는데 빼먹은 경우를 그대로 받아드리고, 어떤 컴파일러는 typename 쓰였지만 원래는 허용되지 않는 경우를 내버려두기도 한다. 외에도 typename 쓰였고 문맥상 써야하는 부분인데도 typename 거부하는 컴파일러도 있다. 그래서 이런 부분은 프로그램을 다른 환경에 이식할 문제가 있으니 주의하자.


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