47: 타입에 대한 정보가 필요하다면 특성정보 클래스를 사용하자
STL의 유틸리티라고 불리는 템플릿의 advance라는 템플릿을 보자. 이 템플릿은 지정된 반복자를 지정된 거리만큼 이동시키는 것이다.
template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d);
STL 반복자는 지원하는 연산에 따라 다섯 개의 범주로 나뉜다.
- 입력 반복자(input iterator)
- 전진만 가능하고, 한 번에 한 칸씩만 이동하며, 자신이 가리키는 위치에서 읽기만 가능하다. 읽을 수 있는 횟수는 한 번뿐이다.
- 출력 반복자(output iterator)
- 입력 반복자와 비슷하지만 출력용인 점만 다르다. 쓰기만 가능하다.
- 순방향 반복자(forward iterator)
- 입력 반복자와 출력 반복자가 하는 일은 다 할 수 있고, 여기에 덧붙여 자신이 있는 위치에서 읽기 쓰기를 동시에 할 수 있으며, 여러 번 가능하다.
- 양방향 반복자(bidirectional iterator)
- 순방향 반복자에 뒤로 갈 수 있는 기능을 추가한 것이다. STL의 list에 쓰는 반복자가 이 범주에 들어간다. set, multiset, map, multimap
- 임의 접근 반복자(random access iterator)
- 양방향 반복자에 "반복자 산술 연산(iterator arthmetic)" 수행 기능을 추가한 것이다. 임의의 거리만큼 반복자를 앞뒤로 이동시키는 일을 상수 시간 안에 할 수 있다. vector, deque, string
C++ 표준 라이브러리에는 범주 각각을 식별하는 데 쓰이는 태그 구조체가 정의되어 있다.
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 {};
상속 관계가 is-a관계이다.
이제 advance로 돌아와서, 이렇듯 반복자들이 종류마다 가능한 것이 있고 불가능한 것이 있다는 점을 안 이상, 구현할 때 조금 더 신경을 써야 한다. 한 가지 방법은 최소 공통 분모(lowest-common-denominator) 전략을 들 수 있다. 주어진 횟수만큼 반복하는 루프를 돌리는 것이다. 하지만 선형 시간이 걸린다. 상수 시간의 산술 연산을 쓸 수 있는 임의 접근 반복자 입장에서는 손해이다. 그래서 임의 접근 반복자가 주어졌을 때는 상수 시간 연산을 이용할 수 있는 방법이 있었으면 좋겠다.
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++)
--tier;
}
}
}
iter의 타입인 IterT가 어떤 타입인지 알아야 한다. 이를 위한 것이 특성정보(traits)이다. 특성정보란, 컴파일 도중에 어떤 주어진 타입의 정보를 얻을 수 있게 하는 객체를 지칭하는 개념이다. C++ 구현 기법이며, 관례이다.
특성정보는 기본제공 타입과 사용자 정의 타입에서 모두 돌아가야 한다. 이를테면 advance는 포인터 및 int를 받아서 호출될 때도 제대로 동작할 수 있어야 한다. 특성정보를 다루는 표준적인 방법은 해당 특성정보를 템플릿 및 그 템플릿의 1개 이상의 특수화 버전에 넣는 것이다. 반복자의 경우, 표준 라이브러리의 특성정보용 템플릿이 iterator_traits라는 이름으로 준비되어 있다.
template<typename IterT>
struct iterator_traits;
iterator_traits<IterT> 안에는 IterT 타입 각각에 대해 iterator_category라는 이름의 typedef 타입이 선언되어 있다. 이렇게 선언된 typedef 타입이 바로 IterT의 반복자 범주를 가리키는 것이다.
iterator_traits 클래스는 이 반복자 범주를 두 부분으로 나누어 구현한다. 첫 번째 부분은 사용자 정의 반복자 타입에 대한 구현이다. 사용자 정의 반복자 타입으로 하여금 iterator_category라는 이름의 typedef 타입을 내부에 가질 것을 요구사항으로 둔다.
template<...>
class deque {
public:
class iterator {
public:
typedef random_access_iterator_tag iterator_category;
...
};
...
};
이 iterator 클래스가 내부에 지닌 중첩 typedef 타입을 앵무새처럼 똑같이 재생한 것이 iterator_traits이다.
template<typedef IterT>
struct iterator_traits {
typedef typename IterT::iterator_category iterator_category;
};
하지만 위의 코드는 반복자의 실제 타입이 포인터인 경우에는 돌아가지 않는다. 이를 지원하기 위해, iterator_traits는 포인터 타입에 대한 부분 템플릿 특수화(partial template specialization) 버전을 제공한다.
template<typedef IterT>
struct iterator_traits<IterT*> {
typedef random_access_iterator_tag iterator_category;
};
특성정보 클래스의 설계 및 구현 방법은 다음과 같다.
- 다른 사람이 사용하도록 열어 주고 싶은 타입 관련 정보를 확인한다(예를 들어, 반복자라면 반복자 범주 등이 여기에 해당된다).
- 그 정보를 식별하기 위한 이름을 선택한다(예: iterator_category).
- 지원하고자 하는 타입 관련 정보를 담은 템플릿 및 그 템플릿의 특수화 버전(예: iterator_traits)을 제공한다.
이제 advance의 의사코드는 다음과 같이 다듬을 수 있다.
template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d)
{
if(typeid(typename std::iterator_traits<IterT>::iterator_category)
== typeid(std::random_access_iterator_tag))
...
}
하지만 아직 부족하다. 첫 번째는 컴파일 문제인데, 이 부분은 항목48에서 다룬다. 다른 문제를 보자. IterT의 타입을 파악하는 때는 컴파일 도중이다. 하지만 if문은 프로그램 실행 도중에 평가된다. 컴파일 도중에 할 수 있는 것을 굳이 실행 도중에 해야 할 이유는 없다. 시간 낭비면서 실행 코드의 크기가 비대해진다. 이를 해결할 수 있는 것은 오버로딩이다.
advance의 "동작 원리 알맹이"는 똑같게 하고, 받아들이는 iterator_category객체의 타입을 다르게 해서 오버로드 함수를 만든다.
template<typename IterT, typename DistT>
void doAdvance(IterT& iter, DistT d, std::random_access_iterator_tag)
{
iter += d;
}
template<typename IterT, typename DistT>
void doAdvance(IterT& iter, DistT d, std::bidirectional_iterator_tag)
{
if (d >= 0) {
while (d--)
++iter;
}
else {
while (d++)
--tier;
}
}
template<typename IterT, typename DistT>
void doAdvance(IterT& iter, DistT d, std::input_iterator_tag)
{
if (d < 0) {
throw std::out_of_range("Negative distance");
}
while (d--)
++iter;
}
이제 advance가 해 줄 일은 오버로딩된 doAdvance를 호출하는 것뿐이다. 이때 컴파일러가 오버로딩 모호성 해결을 통해 적합한 버전을 호출할 수 있도록 반복자 범주 타입 객체를 맞추어 전달해야 한다.
template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d)
{
doAdvance(iter, d, std::iterator_traits<IterT>::iterator_category());
}
마지막으로 특성정보 클래스를 어떻게 사용하는지 정리해 보자.
- "작업자(worker)" 역할을 맡을 함수 혹은 함수 템플릿(예: doAdvance)을 특성정보 매개변수를 다르게 하여 오버로딩한다. 그리고 전달되는 해당 특성정보에 맞추어 각 오버로드 버전을 구현한다.
- 작업자를 호출하는 "주작업자(master)" 역할을 맡을 함수 혹은 함수 템플릿(예: advance)을 만든다. 이때 특성정보 틀래스에서 제공되는 정보를 넘겨서 작업자를 호출하도록 구현한다.
이것만은 잊지 말자!
- 특성정보 클래스는 컴파일 도중에 사용할 수 있는 타입 관련 정보를 만들어낸다. 또한, 특성정보 클래스는 템플릿 및 템플릿 특수 버전을 사용하여 구현한다.
- 함수 오버로딩 기법과 결합하여 특성정보 클래스를 사용하면, 컴파일 타임에 결정되는 타입별 if...else 점검문을 구사할 수 있다.
'C++ > Effective C++' 카테고리의 다른 글
[Effective C++] 49: new 처리자의 동작 원리를 제대로 이해하자 (1) | 2023.11.21 |
---|---|
[Effective C++] 48: 템플릿 메타프로그래밍, 하지 않겠는가? (0) | 2023.11.18 |
[Effective C++] 46: 타입 변환이 바람직할 경우에는 비멤버 함수를 클래스 템플릿 안에 정의해 두자 (0) | 2023.11.16 |
[Effective C++] 45: "호환되는 모든 타입"을 받아들이는 데는 멤버 함수 템플릿이 직방! (0) | 2023.11.15 |
[Effective C++] 44: 매개변수에 독립적인 코드는 템플릿으로부터 분리시키자 (1) | 2023.11.13 |