본문 바로가기
서적 정리/Effective C++

47.타입에 대한 정보가 필요하다면 특성정보 클래스를 사용하자

by 민돌이2 2021. 12. 25.

STL의 유틸리티(utility)도 컨테이너(container) 및 반복자(iterator)처럼 템플릿으로 구현되어 있는데 유틸리티의 함수중 advance라는 템플릿 함수는 반복자를 지정된 거리만큼 이동시키는 것이다.

std::list가 at이 구현이 안되어 있는데 advance를 사용하면 at도 구현할 수 있을 것이다. 나는 반복문 돌려서 ++iter했지만 지금 다시 list를 구현해보라고 하면 advance 사용하는거 고려해 볼 법하다.

template<class _InIt, class _Diff>
_CONSTEXPR17 void advance(_InIt& _Where, _Diff _Off);

실제로 xutility에 이렇게 구현되어 있다.

 

만약 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;
		}
	}
}

임의 접근 방법자같은 반복자는 아래에 STL 반복자로 따로 정리했으니 아래 먼저 보면 된다.

위 코드가 정상 작동하려면 첫 번째 전제 조건은 iter의 타입인 IterT가 임의 접근 반복자인지 아닌지를 판별할 수 있어야 한다. 즉, 어떤 타입에 대한 정보를 얻어낼 필요가 있는데 이것을 특성정보(traits)라고 한다.

 

 

특성정보(traits)

특성 정보는 컴파일 도중에 어떤 주어진 타입의 정보를 얻을 수 있게 하는 객체를 지칭하는 개념일 뿐이다.

C++안에 정의된 문법구조가 아니고 키워드도 아니다. 프로그래머들이 따르는 구현 기법이며, 관례일 뿐이다. 특성정보가 되려면 몇 가지 요구사항이 지켜져야 하는데, 특성정보는 기본제공 타입과 사용자 정의 타입에서 모두 돌아가야 한다는 점이 그 중 하나이다.

 

특수정보를 다루는 표준적인 방법은 해당 특성정보를 템플릿 및 그 템플릿의 1개 이상의 특수화 버전에 넣는 것이다.

반복자의 경우, 표준 라이브러리의 특성정보용 템플릿이 iterator_traits라는 이름으로 구현되어 있다.

template<class _Iter>
	struct iterator_traits
		: _Iterator_traits_base<_Iter>
	{	// get traits from iterator _Iter, if possible
	};

xutility에 실제로 구현되어 있는 코드이다.

특성정보는 항상 구조체로 구현하는 것이 관례지만, 특성정보를 구현하는 데 사용한 구조체를 가르켜 특성정보 클래스라고 부른다.

 

iterator_traits 클래스의 동작 방법

iterator_traits<_IterT> 안에는 IterT 타입 각각에 대해 iterator_category라는 이름의 typedef 타입이 선언되어 있다.

typedef 타입이 바로 IterT의 반복자 범주를 가리키는 것이다.

 

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

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<class _Iter>
struct iterator_traits
{
	typedef typename IterT::iterator_category iterator_category;
	...
};

 

위 코드는 사용자 정의 타입에 대해서는 문제가 없지만, 반복자의 실제 타입이 포인터인 경우에는 문제가 생긴다. 포인터 안에 typedef 타입이 중첩된다는 것부터 문제이다.

 

iterator_traits 구현의 두 번째 부분은 반복자가 포인터인 경우의 처리이다.

포인터 타입의 반복자를 지원하기 위해, iterator_traits는 포인터 타입에 대한 부분 템플릿 특수화(partial template specializaion) 버전을 제공하고 있다. 포인터는 임의 접근 반복자와 동작 원리가 사실상 똑같으므로, iterator_traits가 이런 식으로 지원하는 반복자 범주가 임의 접근 반복자이다.

template<class _Iter>
struct iterator_traits<_Iter*>
{
	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문은 프로그램 실행 도중에 평가된다. 또한 실행 코드의 크기도 거대해진다.

현재 해결 방법은 컴파일 도중에 수행하는 조건처리 구문요소(if...else 문)이다. 오버로딩으로 해결할 수 있다.

template<typename IterT, typename DistT> //임의 접근 반복자
void AdvanceHelper(IterT& iter, DistT d, std::random_access_iterator_tag)
{
	iter += d;
}

template<typename IterT, typename DistT> //양방향 반복자
void AdvanceHelper(IterT& iter, DistT d, std::bidirectional_iterator_tag)
{
	if (d >= 0)
	{
		while (d--)
			++iter;
	}
	else
	{
		while (d++)
			--iter;
	}
}

template<typename IterT, typename DistT> //입력 반복자
void AdvanceHelper(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)
{
	//오버로드한 AdvanceHelper호출
	AdvanceHelper(iter, d, typename std::iterator_traits<IterT>::iterator_category());
}

순방향 반복자(forward_iterator_tag)는 입력 반복자로부터 상속 받는 것이니, input_iterator_tag를 매개변수로 받는 AdvanceHelper 함수는 순방향 반복자도 받을 수 있을 것이다(파생 클래스 암시적 변환).

 

 

특성정보 클래스의 설계 순서 및 정리

특성 클래스의 설계 순서

1.다른 사람이 사용하도록 열어 주고 싶은 타입 관련 정보를 확인한다.

반복자라면 반복자 범주 등이 여기에 해당한다.

 

2.그 정보를 식별하기 위한 이름을 선택한다.

위 코드에서는 iterator_category로 쓰였다.

 

3.지원하고자 하는 타입 관련 정보를 담은 템플릿 및 그 템플릿의 특수화 버전을 제공한다.

위 코드에서는 iterator_traits로 쓰였다.

 

특성 정보 클래스의 사용법

1.작업자(worker) 역할을 맡은 함수 혹은 함수 템플릿을 특성정보 매개변수를 다르게 하여 오버로딩 한다.

위 코드에서 AdvanceHelper

 

2.전달되는 해당 특성정보에 맞춰 각 오버로드 버전을 구현한다.

위 코드에서 AdvanceHelper

 

3.작업자를 호출하는 주작업자(master) 역할을 맡을 함수 혹은 함수 템플릿을 만든다.

위 코드에서 advance

 

4.특성정보 클래스에서 제공되는 정보를 넘겨서 작업자를 호출하도록 구현한다.

위 코드에서 advance에서 AdvanceHelper를 호출하였다.

 

특성정보라는 단어 자체를 나는 처음 들었지만, C++ 표준 라이브러리에는 엄청 많다고 한다.

문자 타입에 대한 정보를 담고 있는 char_traits, 숫자 타입에 대한 정보를 갖고 있는 numeric_limits 또한 특성정보이다.

또한 TR1(54장 참고)이 도입되면서 타입 관련 정보를 제공하는 특성정보 클래스가 상당수 추가되었는데, is_base_of<T1,T2>(T1이 T2와 같거나 T2의 기본 클래스인지 알려줌), is_array<T>(T가 배열 타입인지 알려줌), is_fundamental<T>(T가 기본제공 타입인지 알려줌) 등이 있다.

 

 

STL 반복자

STL 반복자는 각 반복자가 지원하는 연산에 따라 다섯 개의 범주로 나눈다.

1.입력 반복자(input iterator)

전진만 가능하고 한 번에 한 칸씩만 이동하며, 자신이 가리키는 위치에서 읽기만 가능하고, 읽을 수 있는 횟수도 한 번 뿐이다. C++ 표준 라이브러리의 istream_iterator가 대표적인 입력 반복자이다.

 

2.출력 반복자(output iterator)

입력 반복자와 비슷하지만 출력용인 점이 다르다. 즉, 전진만 가능하고, 한 번에 한 칸씩만 이동하며, 자신이 가리키는 위치에서 쓰기만 가능하고, 쓸 수 있는 횟수도 한 번 뿐이다. C++ 표준 라이브러리의 ostream_iterator가 대표적인 출력 반복자이다.

 

3.순방향 반복자(forward iterator)

입력 반복자와 출력 반복자가 하는 일은 기본적으로 다 할 수 있고, 더 나아가 자신이 가리키는 위치에서 여러 번 쓰고 읽을 수 있다. 다중 패스 알고리즘에 문제 없이 사용할 수 있다. STL은 기본적으로 단일 연결 리스트(single linked list)를 제공하지 않지만, slist가 대표적인 순방향 반복자이다. TR1의 해시 컨테이너(54장 참고)를 가리키는 반복자도 순방향 반복자의 범주이다.

 

4.양방향 반복자(bidirectional iterator)

순방향 반복자에서 뒤로 갈 수 있는 기능을 추가한 것이다. STL의 list에 쓰는 반복자가 양방향 반복자의 범주이다. 또한 set, multiset, map, multimap 등의 컨테이너에도 양방향 반복자를 사용한다.

 

5.임의 접근 반복자(random access iterator)

양방향 반복자에서 반복자 산술 연산(iterator arithmetic) 수행 기능을 추가한 것이다. 간단히 말해서 반복자를 임의의 거리만큼 앞뒤로 이동시킬 수 있다. C++ 표준 라이브러리의 vector, deque, string에 사용하는 반복자는 임의 접근 반복자이다.

 

C++ 표준 라이브러리에는 다섯 개의 반복자 범주 각각을 식별하는 태그(tag) 구조체가 정의되어 있다.

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 {};

 

 

이것만은 잊지 말자

특성정보 클래스는 컴파일 도중에 사용할 수 있는 타입 관련 정보를 만들어낸다. 또한 특성정보 클래스는 템플릿 및 템플릿 특수 버전을 사용하여 구현한다.

함수 오버로딩 기법과 결합하여 특성정보 클래스를 사용하면, 컴파일 타임에 결정된느 타입별 if...else 점검문을 구사할 수 있다.

728x90

댓글