성공할 게임개발자

[Effective c++] [7. 템플릿과 제네릭 프로그래밍] 47. 타입에 대한 정보가 필요하다면 특성정보 클래스를 사용하자 본문

C++

[Effective c++] [7. 템플릿과 제네릭 프로그래밍] 47. 타입에 대한 정보가 필요하다면 특성정보 클래스를 사용하자

fn000 2025. 6. 29. 21:22

STL 반복자는 지원하는 연산에 따라 5개의 범주로 나뉜다.

 

입력반복자

읽기전용

단방향 전진만 가능

std::istream_iterator<int> it(std::cin);
int x = *it;  // 읽기 가능

 

 

출력반복자

쓰기전용

단방향 전진만 가능

std::ostream_iterator<int> out(std::cout, " ");
*out = 10;  // 쓰기 가능

 

 

순방향 반복자

읽기, 쓰기 가능

여러번 읽기 가능

단방향 전진

std::istream_iterator<int> it(std::cin);
int x = *it;  // 읽기 가능

 

 

양방향 반복자

순방향에서 뒤로 갈 수 있는 기능 추가한 것

대표 컨테이너 : set, multiset, map, multimap, list

std::list<int>::iterator it;
++it; --it;

 

 

임의 접근 반복자

양방향 반복자에 반복자 산술 연산 수행 기능 추가

산술연산

인덱스 접근

비교연산

가장 빠르고 강력함

대표 컨테이너 : vector, deque, string

std::vector<int>::iterator it;
it = it + 3;
int x = it[1];

 

 

 

 

5개의 반복자 범주 식별을 위한 태그 구조체 

struct input_iterator_tag {};
struct output_iterator_tag {};
struct forward_iterator_tag : public input_iterator_tag {};
struct bidriectional_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;
    }
    // 다른 종류의 반복자에 대해서는 ++ 혹은 -- 연산의 반복 호출을 사용. n 시간 복잡도
    else 
    {
        if(d >= 0) { while(d--)  ++iter;}
        else { while(d++) --iter;}
    }
}

 

위의 코드가 제대로 작동하려면 iter 부분이 임의 접근 반복자인지 판단할 수 있어야한다. 즉, iter 타입인 IterT가 임의 접근 반복자 타입인지를 알아야한다는 것이다. 이걸 알게 해주는 존재가 특성정보이고 특성정보란, 컴파일 도중에 어떤 주어진 타입의 정보를 얻을 수 있게 하는 객체를 지칭하는 개념이다.

 

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

 

 

 

특성 정보를 다루는 표준적인 방법은 해당 특성 정보를 템플릿 및 그 템플릿의 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 타입으로 
        typedef random_access_iterator_tag iterator_category;
        ...
    };
    ...
};

 

 

list 클래스에 쓸 수 있는 반복자

양방향 반복자를 사용한다.

// list의 반복자 태그 구현
template<...>
class list
{
    class iterator
    {
    public:
        // 양방향 반복자 태그를 typedef 타입으로
        typedef bidirectional_iterator_tag iterator_category;
        ...
    };
    ...
};

 

 

사용자 정의타입의 경우

// 사용자 정의 타입에 대한 특성정보 클래스
template<typename IterT>
struct iterator_traits
{
    // typedef typename는 이중 의존이라 사용
     typedef typename IterT::iterator_category iterator_category;
     ...
 };

하지만 반복자의 실제 타입이 포인터인 경우에는 안 돌아간다. iterator_traits 구현의 두 번째 부분은 반복자가 포인터인 경우의 처리

 

 

부분 템플릿 특수화

포인터 타입의 반복자를 지원하기 위함.

// 기본제공 포인터 타입에 대한 부분 템플릿 특수화
template<typename IterT>
struct iterator_traits<IterT*>
{
     typedef random_access_iterator_tag iterator_category;
     ...
 };

 

 

따라서 특성정보 클래스의 설계 및 구현 방법 프로세스는 다음과 같다.

 

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

 

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

 

3. 지원하고자 하는 타입 관련 정보를 담은 템플릿 및 그 템플릿의 특수화 버전을 제공한다.(ex. 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의 타입은 컴파일 도중에 파악되어야 하는데 if-else문을 쓰면 런타임에 검사된다. 따라서 이걸 컴파일에 해결하기 위하여 컴파일타임에 if-else 처럼 쓸 오버로딩을 사용한다. doadvance를 오버로드 함수로 만든다.

 

 

 

// 반복자에 대해서는 이 구현
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_access_iterator_tag)
{
    if(d >= 0) { whild (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)
{
    // iter 반복자 범주에 적합한 doAdvance의 오버로드 버전 호출(컴파일타임)
    doAdvance(iter, d, typename std::iterator_traits<IterT>::iterator_category());
}

 

 

 

중요포인트!

 

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

 

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

 

솔직히 이번내용 잘 이해가 안간다. 꾸준히 보면서 와닿을 때 까지 다시 보는걸로...