KoreanFoodie's Study

Effective C++ | 항목 47 : 타입에 대한 정보가 필요하다면 특성정보 클래스를 사용하자 본문

Tutorials/C++ : Advanced

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

GoldGiver 2022. 10. 25. 16:33

C++ 프로그래머의 필독서이자 바이블인, 스콧 마이어스의 Modern Effective C++ 를 읽고 기억할 내용을 요약하고 있습니다. 꼭 읽어보시길 추천드립니다!

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

핵심 :

1. 특성정보 클래스는 컴파일 도중에 사용할 수 있는 타입 관련 정보를 만들어낸다. 또한 특성정보 클래스는 템플릿 및 템플릿 특수 버전을 사용하여 구현한다.
2. 함수 오버로딩 기법과 결합하여 특성정보 클래스를 사용하면, 컴파일 타임에 결정되는 타입별 if...else 점검문을 구사할 수 있다.

 

STL 에는 container, iterator, algorithm 말고도 utility 라고 불리는 템플릿도 몇 개 들어 있다. 이들 중 advance 라는, 지정된 반복자를 지정된 거리(distance) 만큼 이동시키는 녀석을 보자.

template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d);

사실 iterator 는 다섯 종류가 있다 : (contiguous iterator 는 일단 제외하자. C++20 부터의 기능이다)

  1. 입력 반복자(input iterator) : 전진만 가능, 한 번에 한 칸씩 이동, 가리키는 위치에서 읽기만 가능, 읽을 수 있는 횟수는 한 번
  2. 출력 반복자(output iterator) : 입력 반복자와 비슷하지만 출력용이라 쓰기만 가능
  3. 순방향 반복자(forward iterator) : 읽기 쓰기 여러 번 가능, 전진만 가능
  4. 양방향 반복자(bidirectional iterator) : 읽기 쓰기 여러 번 가능, 전진/후진 가능
  5. 임의 접근 반복자(random access iterator) : 반복자를 임의의 거리만큼 이동 가능. 제일 강력
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 { };
struct contiguous_iterator_tag : public random_access_iterator_tag { }; // (since C++20)

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

 

이제 다시 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++ 에 미리 정의된 문법구조가 아니며, 키워드도 아니다. 그냥 프로그래머들이 따르는 구현 기법이자 관례이다. 특성정보는 기본 제공 타입에 대해서 쓸 수 있어야 하는데, 이는 중첩된 정보 등으로는 구현이 안된다는 말과도 같다. 결국, 어떤 타입의 특성정보는 그 타입의 외부에 존재하는 것이어야 한다. 

// 반복자 타입에 대한 정보를 나타내는 템플릿
template<typename IterT>
struct iterator_traits;

보다시피, iterator_traits 는 구조체 템플릿이다. 관례에 따라, 특성정보는 항상 구조체로 구현하는 것으로 굳어져 있다. 또한 이런 구조체를 가리켜 '특성정보 클래스'라고 부른다.

이는 특성 정보는 두 부분으로 나뉘어져 있다. 먼저, 사용자 정의 반복자 타입으로 하여금 iterator_category 라는 typedef 타입이 선언될 것을 요구사항으로 둔다. 또한 이 iterator 클래스가 내부에 지닌 typedef 타입을 앵무새처럼 똑같이 재생한 것이 iterator_traits 이다.

// 매개변수는 편의상 생략...
template<...>
class deque
{
public:
  class iterator
  {
  public:
    typedef random_access_iterator_tag iterator_category;
  };
};

// 반복자 타입에 대한 정보를 나타내는 템플릿
template<typename IterT>
struct iterator_traits
{
  typedef typename IterT::iterator_category iterator_category;
};

// 포인터 타입의 반복자 지원
// 부분 템플릿 특수화(partial template specialization) 버전
template<typename IterT>
struct iterator_traits<IterT*>
{
  typedef random_access_iterator_tag iterator_category;
};

 

이제 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))
  {
    // 임의 접근 반복자일때의 동작 수행
  }
  ...
}

if ... else 문의 처리는 런타임에 수행하지만, 사실 우리는 위의 타입 체킹이 컴파일 타임에 이루어졌으면 한다(추가로, 이 코드는 컴파일 문제가 있는데, 이는 항목 48에서 다루겠다).

오버로딩을 사용하면 우리가 원하는 것을 얻을 수 있다.

// 임의 접근 반복자
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;
}

이제 advance 를 수정하자. 오버로딩된 doAdvance 를 호출하기만 하면 된다.

template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d)
{
  // iter 의 반복자 타입에 맞는 doAdvance 의 오버로드 버전을 호출한다
  doAdvance
  (
    iter, d,
    typename std::iterator_traits<IterT>::iterator_category()
  );
}

정리하면, 특성정보 클래스 사용은 다음과 같은 2-step 으로 이루어져 있다.

  1. "작업자(worker)" 역할을 맡을 함수 혹은 함수 템플릿(e.g. doAdvance)을 특성정보 매개변수를 다르게 하여 오버로딩한다(오버로드 버전 구현).
  2. 작업자를 호출하는 "주작업자(master)" 역할을 맡을 함수 혹은 함수 템플릿(e.g. advance) 를 만들고, 특성정보 클래스에서 제공되는 정보를 넘겨서 작업자를 호출하도록 구현한다.
Comments