- 특성정보 클래스는 컴파일 도중에 사용할 수 있는 타입 관련 정보를 만들어낸다. 또한 특성정보 클래스는 템플릿 및 템플릿 특수 버전을 사용하여 구현한다.
- 함수 오버로딩 기법과 결합하여 특성정보 클래스를 사용하면, 컴파일 타임에 결정되는 타입별 if...else 점검문을 구사할 수 있다.
타입에 대한 정보는 특성정보 클래스를 사용
STL은 기본적으로 컨테이너 및 반복자, 알고리즘의 템플릿으로 구성되어 있지만, 이 외에 유틸리티라고 불리는 템플릿도 몇 개 들어 있다.
이들 중 하나가 advance라는 이름의 템플릿인데, 이 템플릿이 하는 일은 지정된 반복자를 지정된 거리만큼 이동시키는 것이다.
template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d); // iter를 d만큼 이동
간단히 개념만 놓고 볼 때 advance는 그냥 iter += d만 하면 될 것 같지만, 사실 이렇게 구현할 수 없다.
+= 연산을 지원하는 반복자는 임의 접근 반복자밖에 없기 때문이다.
다른 반복자 타입의 경우에는 ++ 혹은 --연산을 d번 적용하는 것으로 advance를 구현해야 한다.
STL반복자에는 여러 종류가 있다.
STL 반복자는 각 반복자가 지원하는 연산에 따라 다섯 개의 범주로 나뉜다.
- 입력 반복자(input iterator): 전진만 가능하고, 한 번에 한 칸씩만 이동하며, 자신이 가리키는 위치에서 읽기만 가능하다. 또한, 읽을 수 있는 횟수가 한 번뿐이다. 입력 반복자는 입력 파일에 대한 읽기 전용 파일 포인털르 본떠서 만들었고, C++ 표준 하이브러리의 istream_iterator가 대표적인 입력 반복자이다.
- 출력 반복자(output iterator): 입력 반복자와 비슷하지만 출력용인 점만 다르다. ostream_iterator가 대표적이다. 단일 패스 알고리즘에만 사용할 수 있다.
- 순방향 반복자(forward iterator): 입력 반복자와 출력 반복자가 하는 일을 모두 할 수 있으며, 자신이 가리키고 있는 위치에서 읽기 쓰기를 동시에 할 수 있고 여러 번 가능하다. 따라서 다중 패스 알고리즘에 사용할 수 있다. STL에서 단일 연결 리스트를 제공하는 몇몇 라이브러리들에서 반복자로 쓰인다. TR1 해시 컨테이너를 가리키는 반복자가 대표적이다.
- 양방향 반복자(bidrectional iterator): 순방향 반복자에 뒤로 갈 수 있는 기능을 추가한 것이다. STL의 list에 쓰이는 반복자가 여기에 속한다. 이외에도 set, multiset, map, multimap 등의 컨테이너의 반복자로도 쓰인다.
- 임의 접근 반복자(random access iterator): 양방향 반복자에 반복자 산술 연산 수행 기능을 추가한 것이다. 즉, 주어진 반복자를 임의의 거리만큼 앞뒤로 이동시키는 일을 상수 시간에 할 수 있다. STL의 vector, deque, string에 사용되는 반복자이다.
STL에는 지금까지 다섯 개의 반복자 범주를 식별하는 데 쓰이는 태그 구조체가 정의되어 있다.
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가 임의 접근 반복자인지 판단할 수 있어야 한다.
즉, 어떤 타입에 대한 정보를 얻어낼 필요가 있다.
특성정보(traits)를 이용하면 컴파일 도중에 주어진 타입의 정보를 얻을 수 있게 하는 객체를 지칭할 수 있다.
특성정보
특성정보는 C++에 미리 정의된 문법구조가 아니며, 키워드도 아니다.
일종의 관례이다.
특성정보가 되려면 몇 가지 요구사항이 지켜져야 한다.
특성정보는 사용자 정의 타입과 기본제공 타입에 모두 적용할 수 있어야 한다는 것이 그중 하나이다.
예를 들면, advance 포인터나 int를 받아서 호출될 때도 제대로 동작할 수 있어야 한다.
특성정보는 기본제공 타입에 쓸 수 있어야 한다는 것은 어떤 타입 내에 정보로는 구현이 안된다는 뜻이다.
포인터를 보면, 포인터 안에 정보를 넣을 방법이 없다. 결국, 어떤 타입의 특성정보는 그 타입의 외부에 존재하는 것이어야 한다.
특성정보를 다루는 표준적인 방법은 해당 특성정보를 템플릿 및 그 템플릿의 1개 이상의 특수화 버전에 넣는 것이다.
반복자의 경우, 표준 라이브러리의 특성정보용 템플릿인 iterator_traits가 있다.
template<typename IterT>
struct iterator_traits; // 반복자 타입에 대한 정보를 나타내는 템플릿
iterator_traits는 구조체 템플릿이다.
특성정보는 구조체로 구현하는 것이 관례이다.
특성정보를 구현하는 데 사용한 구조체를 가리켜 특성정보 클래스라고 부른다.
iterator_traits 클래스가 동작하는 방법은 다음과 같다.
iterator_traits<IterT>안에는 IterT 타입 각각에 대해 iterator_category라는 이름의 typedef 타입이 선언되어 있다.
이렇게 선언된 typedef 타입이 바로 IterT의 반복자 범주를 가리키는 것이다.
iterator_traits 클래스는 이 반복자 범주를 두 부분으로 나누어 구현한다
첫 번째 부분은 사용자 정의 반복자 타입에 대한 구현으로, 사용자 정의 반복자 타입에게 iterator_category라는 이름의 typedef 타입을 내부에 가질 것을 요구사항으로 둔다.
이때, 이 typedef 타입은 해당 태그 구조체에 대응되어야 한다.
예를 들어, deque의 반복자는 임의 접근 반복자이므로, deque 클래스에 쓸 수 이쓴 반복자는 다음과 같다.
template< ... >
class deque {
public:
class iterator {
public:
typedef random_access_iterator_tag itertor_category;
...
};
...
};
다른 예로, list의 반복자는 양방향 반복자이기 때문에 다음과 같이 되어 있다.
template< .. >
clas list {
public:
class iterator {
public:
typedef bidirectional_iterator_tag iterator_category;
...
};
...
};
iterator 클래스가 내부에 지닌 중첩 typedef 타입을 똑같이 따라한 것이 iterator_traits이다.
template<typename IterT>
struct iterator_traits {
typedef typename IterT::iterator_category itertor_category; // typedef쓰는 이유: 중첩 의존 이름
...
};
위의 코드는 사용자 정의 타입에 대해서는 잘 동작하지만, 반복자의 실제 타입이 포인터인 경우에는 동작하지 않는다.
포인터 안에 typedef 타입이 중첩된다는 것부터가 말이 안 된다.
iterator_traits 구현의 두 번째 부분은 바로 반복자가 포인터인 경우이다.
포인터 타입의 반복자를 지원하기 위해, iterator_traits는 포인터 타입에 대한 부분 템플릿 특수화 버전을 제공하고 있다.
포인터의 동작 원리가 임의 접근 반복자와 같으므로, iterator_traits가 지원하는 반복자 범주가 바로 임의 접근 반복자이다.
template<typename IterT>
struct iterator_traits<IterT*> // 부분 템플릿 특수화
{
typedef random_access_iterator_tag iterator_category;
...
};
특성정보 클래스의 설계 및 구현 방법을 정리하자면 다음과 같다.
- 다른 사람이 사용하도록 열어 주고 싶은 타입 관련 정보를 확인해라. (반복자 범주)
- 그 정보를 식별하기 위한 이름을 선택해라. (iterator_category)
- 지원하고자 하는 타입 관련 정보를 담은 템플릿 및 그 템플릿의 특수화 버전을 제공해라. (iterator_traits)
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문은 프로그램 실행 도중(런타임)에 평가된다.
컴파일 타임에 할 수 있는 것을 런타임에 할 이유가 없다.
지금 상황에서 필요한 것은 주어진 타입에 대한 평가를 컴파일 타임에 수행하는 조건 처리 구문이다.
오버로딩을 이용하면 이 문제를 해결할 수 있다.
오버로딩을 하게 되면 컴파일러는 매개변수를 보고 최적의 오버로드 버전을 결정한다.
컴파일 타임에 타입에 따라 선택되는 조건처리 구문요소인 것이다.
이를 활용하여 advance를 구현하면 된다.
tempalte<typename IterT, typename DistT>
void doAdvance(Iter& iter, DistT d, std::random_access_iterator_tag)
{
iter += d;
}
template<typename IterT, typename DistT>
void doAdvance(Iter& iter, DistT d, std::bidirectional_iterator_tag)
{
if(d >= 0) { while(d--) ++iter; }
else { while(d++) --iter; }
}
template<typename IterT, typename DistT>
void doAdvance(Iter& iter, DistT d, std::input_iterator_tag)
{
if(d < 0)
{
throw std::out_of_range("Negative distance");
}
whle(d--) ++iter;
}
doAdvance 함수의 오버로딩이 되었다.
이제 advance가 오버로딩된 doAdvance를 호출하면 된다.
이때 컴파일러가 오버로딩 모호성 해결을 통해 적합한 버전을 호출할 수 있도록 반복자 범주 타입 객체를 맞추어 전달해야 한다.
template<typename IterT, typename DistT>
void advance(Iter& iter, DistT d)
{
doAdvance(iter, d, typename std::iterator_traits<IterT>::iterator_category());
}
특성정보 클래스 사용법
특성정보 클래스를 어떻게 사용하는지 정리하면 다음과 같다.
- 작업자 역할을 맡을 함수 혹은 함수 템플릿을 특성정보 매개변수를 다르게 하여 오버로딩한다. 그리고 전달되는 해당 특성정보에 맞추어 각 오버로드 버전을 구현한다.
- 작업자를 호출하는 주작업자 역할을 맡을 함수 혹은 함수 템플릿을 만든다. 이때 특성정보 클래스에서 제공되는 정보를 넘겨서 작업자를 호출하도록 구현한다.
특성정보는 C++ 표준 라이브러리에서 자주 사용된다.
iterator_traits는 iterator_category 말고도 제공하는 반복자 관련 정보가 존재한다.
또한 문자 타입에 대한 정보를 담고 있는 char_traits, 숫자 타입에 대한 정보(표현 가능한 최솟값, 최댓값)를 담고 있는 numeric_limits도 있다.
TR1이 도입되면서 타입 관련 정보를 제공하는 특성정보 클래스가 많이 추가되었다.
is_fundamental<T>(T가 기본제공 타입인지), is_array<T>(T가 배열 타입인지), is_base_of<T1, T2>(T1이 T2와 같거나 T2의 기본 클래스인지) 등이 있다.