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

업데이트:     Updated:

카테고리:

태그:

이 글은 아래의 책을 자세히 정리한 후, 정리한 글을 GPT에게 요약을 요청하여 작성되었습니다.
이펙티브 C++ 제3판, 스콧 마이어스 저자, 곽용재 번역

📦 7. 템플릿과 일반화 프로그래밍

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

✅ typename의 첫 번째 의미: class와 동일

// 1번
template<class T>
class Widget;

// 2번
template<typename T>
class Widget;

핵심:

  • 위 두 코드는 차이가 없다
  • 템플릿 매개변수를 선언할 때, classtypename은 서로 바꾸어 써도 된다

그러나, typename이 반드시 쓰여야 하는 경우가 있다.


✅ 의존 이름과 비의존 이름

// 제대로 된 코드는 아니다.
template<typename C>
// 컨테이너에 있는 두 번째 원소를 출력하는 함수
void print2nd(const C& container) {
	if(container.size() >= 2) {
		C::const_iterator iter(container.begin());

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

의존 이름 (Dependent Name)

C::const_iterator

  • 템플릿 매개변수 C에 따라 달라지는 타입이다
  • 템플릿 매개변수에 종속된 것의존 이름이라고 한다

비의존 이름 (Non-dependent Name)

int

  • int와 같이 템플릿 매개변수와 상관없는 타입 이름비의존 이름이라 한다

📌 의존 이름의 동작

int main() {
	vector<int> v = {1,2,3};
	print2nd(v);

	list<int> l = {1,2,3};
	print2nd(l);
}

의존 이름의 변환:

  • C::const_iterator는 함수 내에서 각각 아래와 같은 타입 이름으로 변환될 것이다
    • vector<int>::const_iterator
    • list<int>::const_iterator
  • 이것이 의존 이름이다

중첩 의존 (타입) 이름

이름이 어떤 클래스 안에 중첩되어 있는 것


⚠️ 문제: 컴파일러의 모호성

struct Foo {
	static int const_iterator;
};

template<typename C>
void print2nd(const C& container) {
	// 이것이 곱셈 연산일 가능성이 생긴다.
	C::const_iterator* x;
	...
}

문제:

  • 타입 이름으로 바뀔 것이라 장담할 수 없다
  • struct Foo { static int const_iterator; } 같은 경우로 인해 컴파일러는,
    C::const_iterator가 변수인지 타입인지 알 수 없게 된다

컴파일러 입장:

  • C::const_iterator* x;가 타입 선언인가?
  • 아니면 C::const_iteratorx의 곱셈 연산인가?

✅ 해결: typename으로 타입 명시

template<typename C>
void print2nd(const C& container) {
	if(container.size() >= 2) {
		typename C::const_iterator iter(container.begin());
		...
	}
}

규칙:

템플릿 안에서 중첩 의존 이름을 참조할 경우, typename 키워드를 붙여주자.


🚨 typename 사용 시 주의점

예시 1: 함수 매개변수

template<typename C>
void f(
	const C& container,          // ❌ typename 쓰면 안 됨
	typename C::iterator iter    // ✅ typename 써야 함
);

규칙:

중첩 의존 이름을 식별하는 데만 typename을 사용하여야 한다.


예시 2: 기본 클래스와 멤버 초기화 리스트

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;
		...
	}
	...
};

중첩 의존 타입 이름이지만, typename을 붙이면 안 되는 경우:

  1. 기본 클래스의 리스트에 있을 경우
  2. 멤버 초기화 리스트 내의 기본 클래스 식별자로 있을 경우

이 외에는 typename이 필요하다.


📊 typename 사용 여부 정리

위치 typename 사용 여부 예시
템플릿 매개변수 선택 (class와 동일) template<typename T>
중첩 의존 타입 이름 필수 typename C::iterator
기본 클래스 리스트 사용 금지 class D: public Base<T>::Nested
멤버 초기화 리스트 사용 금지 : Base<T>::Nested(x)
함수 본문 내 필요 시 사용 typename Base<T>::Nested temp;

✅ 실용 예제 1: iterator_traits

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

코드 분석:

  • iterator_traits“반복자가 어떤 종류의 반복자인지”
    일반적으로 다룰 수 있게 해주는 도구이다
  • iterator_traits<IterT>: IterT 타입 특성 정보를 다룸
  • ::value_type: IterT의 타입 추론
  • temp(*iter): iter의 타입을 temp에 넣음

예시:

  • itervector<int>::iterator라면 temp의 타입은 int가 될 것이다

📌 iterator_traits의 내부 구조

struct iterator_traits {
	typedef typename Iterator::iterator_category iterator_category;
	typedef typename Iterator::value_type value_type;
	typedef typename Iterator::difference_type difference_type;
	typedef difference_type distance_type;
	typedef typename Iterator::pointer pointer;
	typedef typename Iterator::reference reference;
};

핵심 개념:

  • iterator_traits타입 특성 정보를 일반화해 다루는 메커니즘이다
  • Java의 Reflection과 유사하다
    • obj.getClass().getName() 같이 정보를 추출할 때 쓰는 점!

✅ 실용 예제 2: typedef로 간결하게

template<typename IterT>
void workWithIterator(IterT iter) {
	typedef typename iterator_traits<IterT>::value_type value_type;

	value_type temp(*iter);
	...
}

장점:

  • 긴 타입 이름을 typedef로 정의하여 코드 가독성 향상
  • typename은 typedef 선언 시 한 번만 사용

💡 실전 활용 팁

긴 중첩 의존 타입은 typedef로 간소화:

// ❌ 읽기 어려움
template<typename T>
void process(T container) {
	typename std::vector<typename T::value_type>::iterator it;
	...
}

// ✅ 읽기 쉬움
template<typename T>
void process(T container) {
	typedef typename T::value_type ValueType;
	typedef typename std::vector<ValueType>::iterator Iterator;

	Iterator it;
	...
}

🧐 정리

  1. 템플릿 매개변수를 선언할 때, class와 typename은 서로 바꾸어 써도 된다.
    의미상 차이가 없다.
  2. 중첩 의존 타입 이름을 식별하는 용도는 반드시 typename을 사용한다.
    컴파일러가 타입임을 인식하도록 해야 한다.
  3. 중첩 의존 이름을 typename 없이 사용하는 경우:
    • 중첩 의존 이름이 기본 클래스 리스트에 있을 때
    • 멤버 초기화 리스트 내의 기본 클래스 식별자로 있을 때
  4. 긴 중첩 의존 타입 이름은 typedef로 간소화하여 가독성을 높이자.
    한 번만 typename을 사용하고 이후에는 typedef 이름을 사용할 수 있다.

Cpp 카테고리 내 다른 글 보러가기

댓글남기기