[Effective C++] 47. 타입에 대한 정보가 필요하다면 특성정보 클래스를 사용하자
STL은 컨테이너, 반복자, 알고리즘의 템플릿으로 구성되어 있지만, 이 외에 유틸리티라고 불리는 템플릿도 몇 개 들어있다.
이들 중 하나가 advance라는 템플릿인데, 이 템플릿의 역할은 반복자를 지정된 거리만큼 이동시키는 것이다.
template <typename IterT, typename DistT>
void advance(IterT& iter, DistT d); // iter를 d 단위만큼 전진시킨다. d<0이면 iter를 후진시킨다.
간단하게 advance는 그냥 iter += d 만 하면 될 것 같지만, += 연산을 지원하는 반복자는 임의 접근 반복자밖에 없기 때문에, 다른 반복자 타입의 경우에는 ++, -- 연산을 d번 적용하는 것으로 구현해야 한다.
STL 반복자
1. 입력 반복자(input iterator)
전진만 가능하고 한 번에 한 칸씩만 이동하며, 자신이 가리키는 위치에서 읽기만 가능하고 읽을 수 있는 횟수는 한 번뿐이다.
C++ 표준 라이브러리의 istream_iterator가 대표적인 입력 반복자이다.
5대 반복자 범주 가운데 출력 반복자와 함께 기능적으로 제일 처지며, 단일 패스 알고리즘에만 제대로 쓸 수 있다.
2. 출력 반복자(output iterator)
전진만 가능하고 한 번에 한 칸씩만 이동하며, 자신이 가리키는 위치에서 쓰기만 가능하고 쓸 수 있는 횟수는 한 번뿐이다.
ostream_iterator가 대표적인 출력 반복자이다.
5대 반복자 범주 가운데 입력 반복자와 함께 기능적으로 제일 처지며, 단일 패스 알고리즘에만 제대로 쓸 수 있다.
3. 순방향 반복자(forward iterator)
입/출력 반복자가 하는 일은 기본적으로 다 할 수 있고, 자신이 가리키고 있는 위치에 읽기와 쓰기가 모두 가능하며 횟수 제한도 없다.
따라서 다중 패스 알고리즘에 문제없이 쓸 수 있다.
4. 양방향 반복자(bidirectional iterator)
순방향 반복자에 뒤로 갈 수 있는 기능을 추가한 것이다.
5. 임의 접근 반복자(random access iterator)
양방향 반복자에 반복자 산술 연산 수행 기능을 추가한 것이다. 주어진 반복자를 임의의 거리만큼 앞뒤로 이동시키는 일을 상수 시간 안에 할 수 있다.
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를 다음과 같이 구현하고 싶다는 얘기다.
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타입인 IterT가 임의 접근 반복자 타입인지를 알아야 하는데, 이를 위해서 컴파일 도중 어떤 주어진 타입의 정보를 얻을 수 있게 하는 객체인 특성정보(traits)를 사용한다.
특성정보(traits)
특성정보는 컴파일 도중에 주어진 타입의 정보를 얻을 수 있게 하는 객체를 뜻한다.
특성정보는 C++에 미리 정의된 문법구조가 아니며, 키워드도 아니다. 그냥 C++프로그래머들이 따르는 구현 기법이며 관례이다.
특성정보가 되려면 몇 가지 요구사항이 지켜져야 하는데, 특성정보는 기본 제공 타입과 사용자 정의 타입에서 모두 돌아가야 한다는 점이 그중 하나이다.
즉, 특성정보 기법을 포인터 등의 기본 제공 타입에 적용할 수 있어야 한다.
결국 어떤 타입의 특성정보는 그 타입의 외부에 존재해야 한다.
특성정보를 다루는 표준적인 방법은 해당 특성정보를 템플릿 및 그 템플릿의 1개 이상의 특수화 버전에 넣는 것이다.
반복자의 경우 표준 라이브러리의 특성정보용 템플릿이 iterator_traits라는 이름으로 준비되어 있다.
template<typename IterT>
struct iterator_traits; // 반복자 타입에 대한 정보를 나타내는 템플릿
특성정보는 항상 구조체로 구현하는 것이 관례이지만, 특성정보를 구현하는 데 사용한 구조체를 가리켜 특성정보 클래스라고 부른다.
iterator_traits 클래스의 동작 방법
iterator_traits <IterT> 안에는 IterT 타입 각각에 대해 iterator_category라는 이름의 typedef 타입이 선언되어 있다.
typedef타입이 바로 IterT의 반복자 범주를 가리키는 것이다.
iterator_traits클래스는 이 반복자 범주를 두 부분으로 나누어 구현한다.
사용자 정의 반복자 타입에 대한 구현
사용자 정의 반복자 타입으로 하여금 iterator_category라는 이름의 typedef타입을 내부에 가질 것을 요구사항으로 두고 이 typedef타입은 해당 태그 구조체에 대응된다.
예를 들어, deque의 반복자는 임의 접근 반복자이고 list는 양방향 반복자일 것이다.
template<...>
class deque
{
public:
class iterator
{
public:
typedef random_access_iterator_tag iterator_category;
...
};
...
};
template<...>
class list
{
public:
class iterator
{
public:
typedef bidirectional_iterator_tag iterator_category;
...
};
...
};
이 iterator 클래스가 내부에 지닌 중첩 typedef타입을 똑같이 재생한다는 것이 iterator_traits이다.
template<typedef IterT>
struct iterator_traits
{
typedef typename IterT::iterator_category iterator_category;
...
};
위 코드는 사용자 정의 타입에 관해서는 문제없지만 반복자의 실제 타입이 포인터의 경우 문제가 생기는데, 포인터 안에 typedef 타입이 중첩된다는 것부터 말이 안 된다.
반복자가 포인터인 경우의 처리
포인터 타입의 반복자를 지원하기 위해 iterator_traits는 포인터 타입에 대한 부분 템플릿 특수화 버전을 제공한다.
사실 포인터의 동작원리가 임의 접근 반복자와 똑같으므로, iterator_traits가 이런 식으로 지원하는 반복자 범주가 바로 임의 접근 반복자이다.
template<typedef IterT>
struct iterator_traits<IterT*>
{
typedef random_access_iterator_tag 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))
...
}
잘 될 것 같지만 컴파일 도중에 IterT의 타입은 파악되고, iterator_traits <IterT>::iterator_category를 파악할 수 있는 곳도 컴파일 도중이다. 하지만 if문은 프로그램 실행 도중에 평가되는데, 컴파일 도중에 할 수 있는 것을 실행 도중에 할 필요가 없다. 게다가 실행 코드도 비대해진다.
해결 방법은 간단한데, 오버로딩이다.
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++)
--iter;
}
}
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;
}
template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d)
{
//오버로드한 DoAdvance호출
DoAdvance(iter, d, typename std::iterator_traits<IterT>::iterator_category());
}
이렇게 되면 컴파일러는 넘긴 인자를 보고 호출 시 전후관계에 맞게 가장 잘 맞는 오버로드 버전을 골라준다.
특성정보 클래스의 설계 순서 및 정리
특성 클래스의 설계 순서
1. 다른 사람이 사용하도록 열어 주고 싶은 타입 관련 정보를 확인한다.
반복자라면 반복자 범주 등이 여기에 해당한다.
2. 그 정보를 식별하기 위한 이름을 선택한다.
위 코드에서는 iterator_category로 쓰였다.
3. 지원하고자 하는 타입 관련 정보를 담은 템플릿 및 그 템플릿의 특수화 버전을 제공한다.
위 코드에서는 iterator_traits로 쓰였다.
특성 정보 클래스의 사용법
1. 작업자(worker) 역할을 맡은 함수 혹은 함수 템플릿을 특성정보 매개변수를 다르게 하여 오버로딩한다.
위 코드에서 DoAdvance
2. 전달되는 해당 특성정보에 맞춰 각 오버로드 버전을 구현한다.
위 코드에서 DoAdvance
3. 작업자를 호출하는 주작업자(master) 역할을 맡을 함수 혹은 함수 템플릿을 만든다.
위 코드에서 advance
4. 특성정보 클래스에서 제공되는 정보를 넘겨서 작업자를 호출하도록 구현한다.
위 코드에서 advance에서 DoAdvance를 호출하였다.
특성정보는 C++ 표준 라이브러리에서 흔히 쓰이니 알아두자!
요약
- 특성정보 클래스는 컴파일 도중에 사용할 수 있는 타입 관련 정보를 만들어 낸다. 또한 특성정보 클래스는 템플릿 및 템플릿 특수 버전을 사용하여 구현한다.
- 함수 오버로딩 기법과 결합하여 특성정보 클래스를 사용하면, 컴파일 타임에 결정되는 타입별 if... else 점검문을 구사할 수 있다.
'Books > Effective C++' 카테고리의 다른 글
[Effective C++] 49. new 처리자의 동작 원리를 제대로 이해하자 (0) | 2022.07.16 |
---|---|
[Effective C++] 48. 템플릿 메타프로그래밍, 하지 않겠는가? (0) | 2022.07.16 |
[Effective C++] 46. 타입 변환이 바람직할 경우에는 비멤버 함수를 클래스 템플릿 안에 정의해 두자 (0) | 2022.07.15 |
[Effective C++] 43. 템플릿으로 만들어진 기본 클래스 안의 이름에 접근하는 방법을 알아 두자 (0) | 2022.07.08 |
[Effective C++] 42. typename의 두 가지 의미를 제대로 파악하자 (0) | 2022.07.08 |
댓글
이 글 공유하기
다른 글
-
[Effective C++] 49. new 처리자의 동작 원리를 제대로 이해하자
[Effective C++] 49. new 처리자의 동작 원리를 제대로 이해하자
2022.07.16 -
[Effective C++] 48. 템플릿 메타프로그래밍, 하지 않겠는가?
[Effective C++] 48. 템플릿 메타프로그래밍, 하지 않겠는가?
2022.07.16 -
[Effective C++] 46. 타입 변환이 바람직할 경우에는 비멤버 함수를 클래스 템플릿 안에 정의해 두자
[Effective C++] 46. 타입 변환이 바람직할 경우에는 비멤버 함수를 클래스 템플릿 안에 정의해 두자
2022.07.15 -
[Effective C++] 43. 템플릿으로 만들어진 기본 클래스 안의 이름에 접근하는 방법을 알아 두자
[Effective C++] 43. 템플릿으로 만들어진 기본 클래스 안의 이름에 접근하는 방법을 알아 두자
2022.07.08