본문 바로가기
프로그래밍/C & C++

[C++] Iterator Traits와 User-Defined Iterators

by 별준 2022. 12. 11.

References

  • C++ Standard Library 2nd

Contents

  • Iterator Traits
  • User-Defined Iterators

Iterator Traits

Iterator Category

반복자에는 위와 같이 다양한 카테고리가 있습니다. 이는 각각이 자신만의 특수한 기능이 있다는 것을 나타냅니다. 서로 다른 반복자 카테고리는 서로의 동작을 오버로딩할 수도 있고, 오버로딩될 수 있습니다. 이때, iterator tags와 iterator traits를 사용하여 이러한 오버로딩을 적용할 수 있습니다.

 

각 반복자 카테고리에서 C++ 표준 라이브러리는 iterator에 대한 "label"처럼 사용되는 iterator tag를 제공합니다.

실제 헤더 파일을 살펴보면 상속이 사용되었다는 것을 볼 수 있습니다. 따라서, forward_iterator_tag는 input_iterator_tag에 속하게 됩니다. output_iterator_tag를 상속하는 카테고리는 없는 것을 볼 수 있는데, 오직 mutable forward iterator만이 output iterator의 요구사항을 만족시킵니다. 다만, 이에 해당하는 카테고리가 존재하지는 않습니다.

 

만약 제너릭 코드를 작성한다면, 카테고리뿐만 아니라 반복자가 가리키는 요소의 데이터 타입 등이 필요할 수 있습니다. 따라서, C++ 표준 라이브러리는 iterator traits를 정의하는 특별한 템플릿 구조체를 제공합니다. 이 구조체에는 반복자에 필요한 정보가 있으며, 반복자가 가져야 하는 모든 데이터 타입(카테고리, 요소의 타입 등)에 대한 공통 인터페이스로 사용합니다.

template<typename _Iterator, typename = __void_t<>>
  struct __iterator_traits { };
template<typename _Iterator>
  struct __iterator_traits<_Iterator,
		     __void_t<typename _Iterator::iterator_category,
			      typename _Iterator::value_type,
			      typename _Iterator::difference_type,
			      typename _Iterator::pointer,
			      typename _Iterator::reference>>
  {
    typedef typename _Iterator::iterator_category iterator_category;
    typedef typename _Iterator::value_type        value_type;
    typedef typename _Iterator::difference_type   difference_type;
    typedef typename _Iterator::pointer           pointer;
    typedef typename _Iterator::reference         reference;
  };

위의 템플릿에서 _Iterator는 반복자의 데이터 타입을 나타내며, 어떤 반복자에서든 코드를 작성할 때 반복자의 카테고리, 요소의 데이터 타입 등을 사용할 수 있습니다. 예를 들어, 아래 표현식을 사용하여 반복자 타입 _Iterator의 값에 대한 데이터 타입을 알아낼 수 있습니다.

typename std::iterator_traits<_Iterator>::value_type

위와 같은 구조에는 두 가지 장점이 있습니다.

  1. 반복자가 모든 데이터 타입 정의를 제공한다는 것을 보장
  2. 특수한 반복자에 대한 (부분) 특수화를 제공

위에서 두 번째 장점은 iterator로 사용될 수 있는 일반적인 포인터를 위한 것 입니다.

/// Partial specialization for pointer types.
template<typename _Tp>
  struct iterator_traits<_Tp*>
  {
    typedef random_access_iterator_tag iterator_category;
    typedef _Tp                         value_type;
    typedef ptrdiff_t                   difference_type;
    typedef _Tp*                        pointer;
    typedef _Tp&                        reference;
  };
/// Partial specialization for const pointer types.
template<typename _Tp>
  struct iterator_traits<const _Tp*>
  {
    typedef random_access_iterator_tag iterator_category;
    typedef _Tp                         value_type;
    typedef ptrdiff_t                   difference_type;
    typedef const _Tp*                  pointer;
    typedef const _Tp&                  reference;
  };

 

Writing Generic Functions for Iterators

Iterator tratis를 사용하면 반복자 카테고리에 따라 다른 코드를 구현하거나 타입 정의를 추론하는 제너릭 함수를 작성할 수 있습니다.

 

Using Iterator Types

Iterator tratis를 사용하는 간단한 예제로 요소에 대한 임시 변수가 필요한 경우를 생각해봅시다. 이때, 임시 값은 다음과 같이 간단히 선언됩니다.

typename std::iterator_traits<T>::value_type tmp;

여기서 T는 반복자의 타입입니다.

 

또 다른 예시로 아래와 같은 요소를 shift하는 알고리즘이 있습니다.

template<typename ForwardIterator>
void shift_left(ForwardIterator beg, ForwardIterator end)
{
  // temporary variable for first element
  typedef typename std::iterator_traits<ForwardIterator>::value_type value_type;
  
  if (beg != end) {
    // save value of first element
    value_type tmp(*beg);
    
    // shift following values
    ...
  }
}

 

Using Iterator Categories

다양한 반복자 카테고리에 따라 서로 다른 구현을 적용하려면 아래의 두 가지 단계를 따라야 합니다.

 

1. 함수 템플릿이 추가 인자로 반복자 카테고리를 받는 함수를 호출하도록 한다.

template<typename Iterator>
inline void foo(Iterator beg, Iterator end)
{
  foo(beg, end, std::iterator_tratis<Iterator>::iterator_category());
}

 

2. 어떤 반복자 카테고리를 위한 다른 함수를 구현하여 특정 반복자 카테고리에 대해서만 제공하도록 한다.

// foo() for bidirectional iterators
template<typename BiIterator>
void foo(BiIterator beg, BiIterator end, std::bidirectional_iterator_tag)
{
  ...
}

// foo() for random-access iterators
template<typename RaIterator>
void foo(RaIterator beg, RaIterator end, std::random_access_iterator_tag)
{
  ...
}

 

Example : distance() 구현

바로 위에서 언급한 각 카테고리를 위한 템플릿 함수 구현의 예시로 반복자에 대한 보조 함수인 distance()를 들 수 있습니다. 이 함수는 두 반복자의 위치와 이들 요소 사이의 거리를 반환합니다. random-access iterators의 경우 단순히 - 연산자를 사용하여 구현할 수 있지만, 다른 모든 반복자에서는 범위의 끝에 도달하기 까지 증가한 숫자를 반환합니다.

// general distance()
template<typename Iterator>
typename std::iterator_traits<Iterator>::difference_type
distance(Iterator pos1, Iterator pos2)
{
  return distance(pos1, pos2, std::iterator_traits<Iterator>::iterator_category());
}

// distance() for random-access iterators
template<typename RaIterator>
typename std::iterator_traits<RaIterator>::difference_type
distance(RaIterator pos1, RaIterator pos2,
         std::random_access_iterator_tag)
{
  return pos2 - pos1;
}

// distance() for input, forward, and bidirectional iterators
template<typename InIterator>
typename std::iterator_traits<InIterator>::difference_type
distance(InIterator pos1, InIterator pos2,
         std::input_iterator_tag)
{
  typename std::iterator_traits<InIterator>::difference_type d;
  for (d=0; pos1 != pos2; ++pos1, ++d) {}
  
  return d;
}

여기서 반복자의 difference type이 반환형으로 사용되었습니다. 그리고 마지막 구현은 input iterator에 대한 태그를 사용했기 때문에 이 구현은 forward와 bidirectional iterators에서도 사용될 수 있습니다.

 

Writing User-Defined Iterators

Iterator Traits를 활용하여 사용자 정의 반복자를 구현해보도록 하겠습니다. 사용자 정의 반복자는 위에서 언급한 iterator tratis를 제공해야 하는데, 아래의 두 가지 방식 중 하나를 선택할 수 있습니다.

  1. 일반 iterator_traits 구조체를 위한 다섯 가지의 필수 데이터 타입을 제공
  2. iterator_tratis 구조체의 (부분) 특수화를 제공

첫 번째 방식을 위해서 C++ 표준 라이브러리에서는 데이터 타입을 정의하는 특별한 베이스 클래스, iterator<>를 제공합니다. 이를 사용하면 단순히 타입만 전달해주면 됩니다.

class MyIterator
  : public std::iterator<std::bidirectional_iterator_tag,
                         type, std::ptrdiff_t, type*, type&> {
  ...
};

위의 클래스 정의에서 첫 번째 템플릿 파라미터는 반복자의 카테고리를 정의하고, 두 번째는 type이라는 요소의 데이터 타입을 정의하고, 세 번째는 difference 타입, 네 번째는 pointer 타입, 다섯 번째는 reference 타입을 정의합니다. 마지막 3개의 인자는 optional이며, 기본값은 ptrdiff_t, type*, type& 입니다.

 

아래 예제 코드를 통해서 사용자 정의 반복자를 어떻게 정의하는지 살펴보도록 하겠습니다. 이 코드에서는 연관/비정렬 컨테이너를 위한 insert iterator를 정의합니다. C++ 표준 라이브러리에서 제공하는 insert iterator와는 달리 삽입하는 지점을 인자로 받지 않습니다.

#include <iterator>

template<typename Container>
class asso_insert_iterator
  : public std::iterator<std::output_iterator_tag, typename Container::value_type>
{
protected:
  Container& container; // container in which elements are inserted

public:
  // constructor
  explicit asso_insert_iterator(Container& c) : container(c) {}

  // assignment operator - inserts a value into the container
  asso_insert_iterator<Container>&
  operator=(const typename Container::value_type& value) {
    container.insert(value);
    return *this;
  }

  // dereferencing is a no-op that returns the iterator itself
  asso_insert_iterator<Container>& operator*() {
    return *this;
  }

  // increment operation is a no-op that returns the iterator itself
  asso_insert_iterator<Container>& operator++() {
    return *this;
  }
  asso_insert_iterator<Container>& operator++(int) {
    return *this;
  }
};

// convenience function to create the inserter
template<typename Container>
inline asso_insert_iterator<Container> asso_inserter(Container& c)
{
  return asso_insert_iterator<Container>(c);
}

위 코드에서 asso_insert_iterator 클래스는 iterator 클래스로부터 파생되었고, 베이스 클래스에 대응되는 데이터 타입이 정의되어 있습니다. iterator 클래스에 전달된 첫 번째 템플릿 인자는 반복자 카테고리를 명시하는 output_iterator_tag 입니다. 두 번째 인자는 반복자가 참조하는 값의 데이터 타입으로 여기서는 컨테이너의 value_type 입니다. output iterator는 무언가를 write하는 데에만 사용될 수 있으므로 타입 정의가 필요하진 않지만, 위 코드처럼 값의 데이터 타입을 전달하면 어떠한 반복자 카테고리에서도 잘 동작합니다.

 

반복자를 생성할 때, 반복자는 container 멤버 변수에 자신의 컨테이너를 저장합니다. 할당되는 어떤 값이든지 이 컨테이너의 insert() 함수를 사용하여 값을 삽입합니다. *와 ++ 연산자는 아무런 동작도 수행하지 않고 단지 반복자 자체를 반환합니다.

 

마지막으로 inserter를 생성하고 초기화할 때 사용할 헬퍼 함수로 asso_inserter를 정의했습니다. 아래 코드는 이렇게 정의한 inserter를 사용해 unordered set에 요소를 삽입하는 예제를 보여줍니다.

int main()
{
  std::unordered_set<int> coll;
  // create inserter for coll (inconvenient way)
  asso_insert_iterator<decltype(coll)> iter(coll);

  // insert elements with the usual iterator interface
  *iter = 1;
  iter++;
  *iter = 2;
  iter++;
  *iter = 3;

  for (auto pos = coll.begin(); pos != coll.end(); ++pos) {
    std::cout << *pos << " ";
  }
  std::cout << std::endl;

  // create inserter for coll and insert elements (convenient way)
  asso_inserter(coll) = 44;
  asso_inserter(coll) = 55;

  for (auto pos = coll.begin(); pos != coll.end(); ++pos) {
    std::cout << *pos << " ";
  }
  std::cout << std::endl;

  // use inserter with an algorithm
  std::vector<int> vals = { 33, 67, -4, 13, 5, 2 };
  std::copy(vals.begin(), vals.end(), asso_inserter(coll));

  for (auto pos = coll.begin(); pos != coll.end(); ++pos) {
    std::cout << *pos << " ";
  }
  std::cout << std::endl;
}

정의한 inserter는 연관 컨테이너에서도 사용될 수 있으며, unordered_set을 set으로 변경해도 프로그램은 잘 동작합니다.

'프로그래밍 > C & C++' 카테고리의 다른 글

[C++] 스트림(stream) 클래스 (1)  (0) 2022.12.13
[C++] 함수 객체 활용  (0) 2022.12.12
[C++] Clocks and Timers  (0) 2022.12.08
[C++] Type Traits와 Type Utilities  (0) 2022.12.07
[C++] Numeric Limits  (0) 2022.12.06

댓글