[C++] 항목 47: 타입에 대한 정보가 필요하다면 특성정보 클래스를 사용하자

업데이트:     Updated:

카테고리:

태그:

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

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

👉🏻 항목 47: 타입에 대한 정보가 필요하다면 특성정보 클래스를 사용하자

✅ 문제 상황: advance 함수 구현

STL은 여러 템플릿으로 구성되어 있다:

  • 컨테이너(container)
  • 반복자(iterator)
  • 알고리즘(algorithm)
  • 유틸리티(utility)
template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d);

advance 함수:

  • 유틸리티 중 하나인 템플릿
  • 지정된 반복자를 지정된 거리만큼 이동시킨다
  • 우리가 구현하고자 하는 함수이다.

📌 STL 반복자의 5가지 종류

1. 입력 반복자 (Input Iterator)

  • ++it (전진, 한칸 이동)
  • 읽기 1번 가능
  • 예: istream_iterator

2. 출력 반복자 (Output Iterator)

  • ++it (전진, 한칸 이동)
  • 쓰기 1번 가능
  • 예: ostream_iterator

3. 순방향 반복자 (Forward Iterator)

  • ++it (전진, 한칸 이동)
  • 읽기/쓰기 여러번 가능
  • 예: slist

4. 양방향 반복자 (Bidirectional Iterator)

  • ++it, -it (전후진, 한칸 이동)
  • 읽기/쓰기 여러번 가능
  • 예: list, set, multiset, map, multimap

5. 임의 접근 반복자 (Random Access Iterator)

  • +, , +=, =, [] 등 지원 (임의 거리 이동을 상수 시간에 가능!)
  • 읽기/쓰기 여러번 가능
  • 예: vector, deque, string

📌 반복자 태그 구조체

struct input_iterator_tag {};
struct output_iterator_tag {};
struct forward_iterator_tag: public input_iterator_tag {};
struct bidirectional_iterator_tag: public forward_iterator_tag {};
struct random_access_iterator_tag: public bidirectional_iterator_tag {};

핵심:

  • C++ 표준 라이브러리에는 반복자를 식별하기 위한 “태그(tag) 구조체”가 정의되어 있다
  • 각각 is-a 관계로 상속이 되어있다 (이후, 이유를 알 수 있음)

⚠️ advance 함수의 구현 문제

template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d) {
	// 임의 접근 반복자라면 산술 연산
	if(iter 임의 접근 반복자) {
		iter += d;
	}
	// 아니라면, ++ 혹은 -- 반복 호출
	else {
		if(d >= 0) { while(d--) ++iter; }
		else { while(d++) --iter; }
	}
}

의도:

  • 반복자마다 특징이 다르기 때문에, 공통적인 특징을 구현하려 하였다
  • 임의 접근 반복자는 +=를 쓸 수 있음
  • 이 외는 사용할 수 없기에 예외를 둠

문제:

그러나 iter가 임의 접근 반복자인지 어떻게 알 수 있을까?

→ 이제 특성 정보(traits)에 대해 알아보자.


✅ 특성 정보(Traits)란?

특성 정보(traits): 컴파일 도중 주어진 타입의 정보를 얻을 수 있게 하는 객체를 지칭하는 개념

요구사항:

  • 기본제공 타입과 사용자 정의 타입에서 모두 특성 정보를 지원해야 한다
  • 기본 제공 타입인 포인터int에도 동작하여야 함

문제:

  • 하지만, 포인터int와 같은 타입에 특성 정보를 넣을 수 없다
  • 즉, 특성 정보는 타입의 외부에 존재해야 함

해결:

  • 템플릿 및 그 템플릿의 1개 이상의 특수화 버전에 넣어야 한다
  • 반복자의 경우, 표준 라이브러리의 특성정보용 템플릿이 iterator_traits 이름으로 있다

📌 iterator_traits 구조

// 반복자 타입에 대한 정보를 나타내는 템플릿
template<typename IterT>
struct iterator_traits {
	// 현재 중요한 것은 여기
	typedef typename IterT::iterator_category iterator_category;

	// 이 외에 다른 변수들도 있다.
	typedef typename IterT::value_type value_type;
	...
};

특성정보 클래스:

  • 위와 같이 특성 정보를 구현하는데 사용한 구조체를 ‘특성정보 클래스’라고 부른다
  • iterator_traits<vector<int>::iterator>::value_type temp(*iter)와 같은 형태로
    변수에 넣어 사용할 수 있다

✅ iterator_traits의 두 가지 구현

iterator_traits 클래스는 반복자 범주를 두 부분으로 나누어 구현한다:

  1. 사용자 정의 반복자 타입
  2. 포인터 타입

1. 사용자 정의 반복자 타입 구현

// deque(임의 접근 반복자를 가짐)
template<...> // 편의상 생략
class deque {
public:
	class iterator {
	public:
		typedef random_access_iterator_tag iterator_category;
		...
	};
	...
};

// list(양방향 반복자를 가짐)
template<...>
class list {
public:
	class iterator {
	public:
		typedef bidirectional_iterator_tag iterator_category;
		...
	};
	...
};

핵심:

  • iterator_traits와 연결하기 위해서, 일련의 작업이 필요하다
  • 사용자 정의 반복자 타입 내에 iterator_category라는 이름의 typedef 타입을 두어야 한다

동작 방법 상세

// deque(임의 접근 반복자를 가짐)
template<...> // 편의상 생략
class deque {
public:
	class iterator {
	public:
		typedef random_access_iterator_tag iterator_category;
		...
	};
	...
};

template<typename IterT>
struct iterator_traits {
	typedef typename IterT::iterator_category iterator_category;
	...
};

int main() {
	deque<int> dq = {1,2,3};
	deque<int>::iterator iter = dq.begin();

	typename iterator_traits<deque<int>::iterator>::iterator_category temp(*iter);
}

단계별 동작:

  1. deque 내의 random_access_iterator_tagiterator_category라는 별칭을 가지게 됨
  2. iterator_traits는 템플릿 매개변수로 deque를 받게되고,
    deque::iterator_categoryiterator_category라는 별칭으로 다시 바뀜
  3. 이제 iteriterator_categorytemp에 저장할 수 있음

2. 포인터 타입 구현

// 기본제공 포인터 타입에 대한 부분 템플릿 특수화
template<typename IterT>
struct iterator_traits<IterT*> {
	typedef random_access_iterator_tag iterator_category;
	...
};

핵심:

  • 포인터 타입의 반복자를 지원하기 위해, iterator_traits부분 템플릿 특수화 버전을 제공한다

📋 특성정보 클래스의 설계 및 구현 방법

  1. 다른 사람이 사용하도록 열어 주고 싶은 타입 관련 정보를 확인한다
    • 반복자라면 반복자 범주
  2. 그 정보를 식별하기 위한 이름을 선택한다
    • 예: iterator_category
  3. 지원하고자 하는 타입 관련 정보를 담은 템플릿 및 그 템플릿의 특수화 버전을 제공한다

⚠️ 잘못된 시도: typeid 사용

template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d) {
	if(typeid(typename iterator_traits<IterT>::iterator_category)
		== typeid(random_access_iterator_tag)
	) {
		iter += d;
	} else {
		if(d >= 0) { while(d--) ++iter; }
		else { while(d++) --iter; }
	}
}

문제점:

  1. 이 코드는 컴파일이 안된다 (이유는 항목 48에서)
  2. 더 큰 문제:
    • IterT의 타입과 iterator_traits<IterT>::iterator_category컴파일 도중 파악된다
    • if문은 런타임에 평가된다
    • 즉, 주어진 타입에 대한 평가를 컴파일 도중 해야한다

✅ 올바른 해결: 함수 오버로딩

template<typename IterT, typename DistT>
void doAdvance(IterT& iter, DistT d,
	random_access_iterator_tag
) {
	iter += d;
}

template<typename IterT, typename DistT>
void doAdvance(IterT& iter, DistT d,
	bidirectional_iterator_tag
) {
	if(d >= 0) { while(d--) ++iter; }
	else { while(d++) --iter; }
}

template<typename IterT, typename DistT>
void doAdvance(IterT& iter, DistT d,
	input_iterator_tag
) {
	if(d < 0) { throw out_of_range("Negative distance"); }
	else { while(d--) ++iter; }
}

template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d) {
	doAdvance(iter, d,
		typename iterator_traits<IterT>::iterator_category()
	);
}

핵심 원리:

  • forward_iterator_taginput_iterator_tag를 상속했다
  • 즉, input_iterator_tag를 매개변수로 받는 doAdvance는, 순방향 반복자도 받을 수 있다
  • 반복자 태그 사이의 상속이 왜 되어있었던 것인지 여기서 알 수 있다
  • advance에서 오버로딩된 doAdvance를 호출함으로써, 이전의 문제를 해결하였다

📋 특성정보 클래스 구현 방법 정리

1. 작업자(worker) 역할 설정

  • 작업자 역할을 맡을 함수 혹은 함수 템플릿(예: doAdvance)을 특성정보 매개변수를 다르게 하여 오버로딩한다
  • 전달되는 해당 특성정보에 맞추어 각 오버로드 버전을 구현한다

2. 주작업자(master) 역할 설정

  • 작업자를 호출하는 주작업자 역할을 맡을 함수 혹은 함수 템플릿(예: advance)를 만든다
  • 특성정보 클래스에서 제공되는 정보를 넘겨서 작업자를 호출하도록 구현한다

📌 TR1의 추가 특성정보 클래스

TR1(항목 52 참조)가 도입되며, 특성정보 클래스가 상당수 추가되었다:

  • is_fundamental<T>: T가 기본제공 타입인지 알려준다
  • is_array<T>: T가 배열 타입인지 알려준다
  • is_base_of<T1, T2>: T1이 T2와 같거나 T2의 기본 클래스인지 알려준다

🧐 정리

  1. 특성정보 클래스는 컴파일 도중에 사용할 수 있는 타입 관련 정보를 만들어 낸다.
    또한 특성정보 클래스는 템플릿 및 템플릿 특수 버전을 사용하여 구현한다.
  2. 함수 오버로딩 기법과 결합하여 특성정보 클래스를 사용하면,
    컴파일 타임에 결정되는 타입별 if…else 점검문을 구사할 수 있다.
  3. 특성정보 클래스는 작업자 함수(worker)와 주작업자 함수(master)의 조합으로 구현한다.
    주작업자는 특성정보를 전달하고, 작업자는 오버로딩을 통해 적절한 버전을 선택한다.
  4. 반복자 태그의 상속 관계는 함수 오버로딩을 통한 자동 타입 선택을 가능하게 한다.
    이것이 태그 구조체들이 is-a 관계로 설계된 이유다.

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

댓글남기기