스토리텔링 개발자

[Effective C++] 47. 특성 정보 클래스 본문

개발/Effective C++

[Effective C++] 47. 특성 정보 클래스

김디트 2024. 7. 19. 12:11
728x90

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

 

 

 

STL의 구성
  • 컨테이너(container) 템플릿
  • 반복자(iterator) 템플릿
  • 알고리즘(algorithm) 템플릿
  • 유틸리티(utility) 템플릿
    • 지정된 반복자를 지정된 거리(distance)만큼 이동시킨다.
    • // iter를 d 단위만큼 양수면 전진, 음수면 후진 시킨다.
      templatce<typename IterT, typename DistT>
      void advance(IterT& iter, DistT d);
    • 그냥 iter += d 만 하면 될 것 같지만..
      • 모든 반복자를 지원해야 할텐데....
      • += 연산자를 지원하는 반복자는 임의 접근 반복자밖에 없다.

 

 

 

STL 반복자의 종류
  1. 입력 반복자(input iterator)
    • 전진만 가능하다.
    • 한번에 한 칸씩만 이동한다.
    • 자신이 가리키는 위치에서 읽기만 가능하다.
    • 읽을 수 있는 횟수가 한 번으로 제약되어 있다.
    • 입력 파일에 대한 읽기 전용 파일 포인터를 본떠서 만들었다.
    • 예) istream_iterator
  2. 출력 반복자(output iterator)
    • 전진만 가능하다.
    • 한번에 한 칸씩만 이동한다.
    • 자신이 가리키는 위치에서 쓰기만 가능하다.
    • 쓸 수 있는 횟수가 한 번으로 제약되어 있다.
    • 출력 파일에 대한 쓰기 전용 파일 포인터를 본떠서 만들었다.
    • 예) ostream_iterator
  3. 순방향 반복자(forward iterator)
    • 전진만 가능하다.
    • 한번에 한 칸씩만 이동한다.
    • 읽기와 쓰기를 모두 할 수 있다.
    • 읽고 쓰는 횟수에 제약이 없다.
    • 예) 해시 컨테이너를 가리키는 반복자
  4. 양방향 반복자(bidirectional iterator)
    • 전진, 후진이 가능하다.
    • 한번에 한 칸씩만 이동한다.
    • 읽기와 쓰기를 모두 할 수 있다.
    • 읽고 쓰는 회수에 제약이 없다.
    • 예) set, multiset, map, multimap 등의 컨테이너의 반복자
  5. 임의 접근 반복자(random access iterator)
    • 전진, 후진이 가능하다.
    • 임의의 거리만큼 앞뒤로 이동시키는 일을 상수시간 안에 할 수 있다.
    • 읽기와 쓰기를 모두 할 수 있다.
    • 읽고 쓰는 회수에 제약이 없다.
    • 기본 제공 포인터를 본떠서 만들었다.
    • 예) vector, deque, string의 반복자

 

 

 

반복자 식별을 위한 태그 구조체
  • 각 반복자에는 자신이 어떤 반복자인지 식별하기 위한 태그(tag) 구조체가 정의되어 있다.
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 {};

 

 

 

유틸리티 템플릿이 모든 반복자를 지원하게 해보자
  • 최소 공통 분모(lowest-common-denominator) 전략을 쓰면 어떨까.
    • 반복자를 주어진 횟수만큼 반복적으로 한 칸씩 증가 / 감소시키는 루프를 돌린다.
    • 선형 시간이 걸린다는 단점이 있다.
      • 상수 시간에 반복자에 접근할 수 있는 임의 접근 반복자 입장에서는 손해다.
    • 아무래도 각 반복자 별로 구현을 달리하는 것이 좋을 것 같다.
  • 즉, 각 타입에 대한 정보를 얻어낼 필요가 있다.

 

 

 

특성정보(traits)
  • 컴파일 도중에 어떤 주어진 타입의 정보를 얻을 수 있게 하는 객체를 지칭하는 개념이다.
  • 요구사항
    • 특성정보는 기본 타입 / 사용자 정의 타입 모두를 지원해야 한다.
      • 즉, advance는 기본 포인터 타입(const char* 등)이나 기본 타입(int)으로 호출하려 할 때도 제대로 동작해야 한다.
      • 뒤집어 생각하면, 어떤 타입 내에 중첩된 정보를 삽입하는 방법으로는 구현이 안 된다는 뜻이다.
        • 기본 타입 내부에는 정보를 넣을 방법이 없기 때문이다.
      • 즉, 특성정보는 그 타입의 외부에 존재해야 한다.

 

 

 

특성정보를 다루는 표준적인 방법
  • 해당 특성정보를 템플릿 및 그 템플릿의 1개 이상의 특수화 버전에 넣는다.
  • 반복자의 경우, iterator_traits라는 이름으로 준비되어 있다.
// 반복자 타입에 대한 정보를 나타내는 템플릿
template<typename IterT>
struct iterator_traits;
  • 특성정보를 구현하는 관례
    • 특성정보는 항상 구조체로 구현하며, 이 특성정보 구조체를 '특성정보 클래스'라고 부른다.

 

 

 

특성정보 클래스의 구현 방법을 iterator_traits로 알아보자
  • iteractor_traits<IterT> 안에는...
    • IterT 타입 각각에 대해 iterator_category라는 이름의 typedef 타입이 선언되어 있다.
    • 이렇게 선언된 typedef 타입이 바로 IterT의 반복자 태그를 가리킨다.
  • iterator_traits 클래스는 반복자의 태그를 두 부분(사용자 정의 타입 / 기본제공 타입)으로 나누어 구현한다.
    • 사용자 정의 반복자 타입에 대한 구현
      • iterator_category라는 이름의 typedef 타입을 내부에 가질 것을 요구사항으로 둔다.
      • 그리고 이 typedef 타입을 특성정보 클래스(iterator_traits)에서는
      • // deque의 반복자 태그 구현
        template<...>
        class deque
        {
        public:
            class iterator
            {
            public:
            	// 임의 접근 반복자 태그를 typedef 타입으로 가진다.
                typedef random_access_iterator_tag iterator_category;
                ...
            };
            ...
        };
        
        // list의 반복자 태그 구현
        template<...>
        class list
        {
            class iterator
            {
            public:
                // 양방향 반복자 태그를 typedef 타입으로 가진다.
                typedef bidirectional_iterator_tag iterator_category;
                ...
            };
            ...
        };
        
        // 사용자 정의 타입에 대한 특성정보 클래스
        template<typename IterT>
        struct iterator_traits
        {
            // typedef typename 에 대한 부분은 항목 42 참조
             typedef typename IterT::iterator_category iterator_category;
             ...
         };
    • 반복자가 포인터인 경우에 대한 구현
      • 이를 처리하기 위해 특성정보 클래스는 부분 템플릿 특수화(partial template specialization) 버전을 제공한다.
      • // 기본제공 포인터에 대한 부분 템플릿 특수화
        template<typename IterT>
        struct iterator_traits<IterT*>
        {
            typedef random_access_iterator_tag iterator_category;
            ...
        }

 

 

 

특성 정보 클래스 설계 및 구현 방법
  1. 다른 사람이 사용하도록 열어주고 싶은 타입 관련 정보를 확인한다.
    • 예) iterator_tag
  2. 그 정보를 식별하기 위한 이름을 선택한다.
    • 예) iterator_category
  3. 지원하고자 하는 타입 관련 정보를 담은 템플릿 및 템플릿 특수화 버전을 제공한다.
    • 예) iterator_traits 템플릿

 

 

 

특성 정보 클래스를 사용하여 유틸리티 템플릿을 구현해보자
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))
    {
        ...
    }
}
  • 해당 코드의 문제점
    • 컴파일 문제
      • 항목 48에서 이어서 살펴보기로 한다.
    • if문은 프로그램 실행 도중에 평가되나, iterartor_traits<IterT>::iterator_category를 파악할 수 있는 건 컴파일 단계이다.
      • 컴파일 도중에 할 수 있는 걸 굳이 실행 도중에 할 이유가 없다.
      • 게다가 실행 코드의 크기도 커진다.
  • 즉, 컴파일 타임에 수행하는 조건처리 구문요소가 필요하다.
    • 여기에 딱 맞는 기능이 바로 C++의 오버로딩.
  • 그러므로 컴파일 단에서 평가할 수 있도록 오버로드를 통해 구현해보자.
  • 구현 방법
    1. '작업자(worker)' 역할을 맡을 함수나 함수 템플릿을 특성정보 매개변수가 다르게 하여 오버로딩한다.
      • 아래의 예에서는 doAdvance가 그 역할이다.
    2. '주작업자(master)' 역할을 맡을 함수나 함수 템플릿을 만들고, 내부에서 작업자에 특성정보 클래스의 정보를 넘기며 호출하게 한다.
      • 아래의 예에서는 Advance가 그 역할이다.
// 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)
{
    doAdvance(iter, d, typename std::iterator_traits<IterT>::iterator_category());
}

 

 

 

C++ 표준 라이브러리에서 사용하는 특성정보들
  • iterator_traits
  • char_traits
    • 문자 타입에 대한 정보
  • numeric_limits
    • 숫자 타입에 대한 정보(표현 가능한 최소값, 최대값 등)
  • is_fundamental<T>
    • T가 기본제공 타입인지를 알려준다.
  • is_array<T>
    • T가 배열타입인지 알려준다.
  • is_base_of<T1, T2>
    • T1이 T2와 같거나 T2의 기본 클래스인지 알려준다.
728x90
Comments