728x90
반응형

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 점검문을 구사할 수 있다.
728x90
반응형