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

[C++] Iterator (이터레이터, 반복자)

by 별준 2022. 2. 21.

References

  • Professional C++

Contents

  • Iterator, Iterator Traits
  • Stream Iterator
  • Iterator Adaptor
  • Reverse Iterator
  • Move Iterator

1. Iterator

표준 라이브러리는 컨테이너의 원소에 접근하는 기능을 범용적으로 제공하기 위해 반복자(이터레이러, iterator) 패턴을 사용합니다. 컨테이너마다 원소에 대해 반복문을 수행할 방법이 담긴 특수한 스마트 포인터인 반복자가 정의되어 있습니다. 컨테이너의 종류가 달라도 반복자의 인페이스는 모두 C++ 표준을 따르므로 모두 같습니다. 그래서 구체적인 동작은 달라도 컨테이너 원소에 대해 반복문을 비슷한 방식으로 작성할 수 있도록 인터페이스는 통일되어 있습니다.

 

반복자는 컨테이너의 특정 원소에 대한 포인터로 생각할 수 있습니다. 배열에서 원소를 가리키는 포인터처럼 반복자도 operator++ 연산자를 이용하여 다음 원소로 이동할 수 있습니다. 또한 반복자로 원소의 필드나 원소 자체에 접근할 때 operator*나 operator->를 사용할 수도 있습니다. 어떤 반복자는 operator=이나 operator!=로 비교하거나 operator--로 이전 원소로 이동하는 기능도 제공합니다.

 

모든 반복자는 반드시 복사 생성자, 복사 대입 연산자, 소멸자를 제공해야 합니다. 반복자의 좌측값(lvalue)는 반드시 맞교환이 가능해야 합니다. 반복자의 기능은 컨테이너마다 약간씩 다른데, 표준에서는 다음 표와 같이 반복자를 6가지 카테고리로 분류하여 정의하고 있습니다.

ITERATOR CATEGORY OPERATIONS REQUIRED COMMENTS
Input (or Read) operator++
operator*
operator->
copy constructor(복사 생성자)
operator=
operator==
operator!=
읽기 전용이며, 정방향(forward)으로만 사용할 수 있다.(역방향으로 이동하는 operator--가 없음)

이 반복자를 대입하거나, 복제하거나, 서로 같은지 비교할 수 있다.
Output (or Write) operator++
operator*
copy constructor
operator=
쓰기 전용이며, 정방향으로만 접근할 수 있다.
이 반복자를 대입할 수는 있지만, 서로 같은지 비교할 수는 없다.
출력 반복자는 *iter = value도 할 수 있다.
operator->가 없다.
operator++에 대한 선행(prefix) 연산자 버전과 후행(postfix) 연산자 버전을 모두 제공한다.
Forward (정방향) input iterator에 디폴트 생성자 추가 읽기 및 정방향 전용이다.
반복자를 대입하거나 복사하거나 서로 같은지 비교할 수 있다.
Bidirectional (양방향) 정방향 반복자의 연산자에 operator-- 연산자 추가 정방향 반복자에서 제공하는 모든 기능을 제공한다.
역방향으로 이동해서 이전 원소에 접근할 수 있다.
operator--에 대한 선행/후행 연산자 버전을 모두 제공한다.
Random access 양방향 반복자의 연산자에 다음의 연산자 추가
operator+
operator-
operator+=
operator-=
operator<
operator>
operator<=
operator>=
operator[]
일반 포인터와 같다. 포인터 산술 연산, 배열 인덱스 구문, 그리고 모든 종류의 비교 연산을 지원한다.
Contiguous Random-access 반복자의 연산자
논리적으로 인접한 요소가 메모리에 물리적으로 인접
std::array, vector(not vector<bool>), string, and string view의 반복자가 이에 속한다.

위 테이블에는 intput, output, forward, bidirectional, random access, contiguous, 총 6 타입의 반복자가 있습니다. 이러한 반복자에 공식적인 계층은 없습니다. 그러나 이 반복자들이 제공하는 기능에 따라서 계층을 추론할 수는 있습니다. 구체적으로, 모든 contiguous 반복자는 random access 반복자이고, 모든 random access 반복자는 bidirectional 이며, 모든 bidirectional 반복자는 forward 반복자이고, 모든 forward 반복자는 input 반복자입니다.

여기서 output 반복자의 요구사항을 만족하는 반복자를 mutable(가변) 반복자라고 부르고, 그렇지 않은 반복자를 constant(상수, 불면) 반복자라고 부릅니다. 

 

알고리즘에서 사용할 반복자의 종류를 표준 방식으로 지정하려면 반복자 템플릿 타입 인수에 InputIterator, OutputIterator, ForwardIterator, BidirectionalIterator, RandomAccessIterator, ContiguousIterator와 같은 이름을 지정하면 됩니다. 말 그대로 이름일 뿐이기 때문에 바인딩 타입 검사를 하지 않습니다. 따라서 RandomAccessIterator를 인수로 받는 알고리즘에 양방향 반복자를 지정해서 호출해도 타입 검사를 거치지 않기 때문에 템플릿을 인스턴스화할 수 있습니다. 하지만 랜덤 액세스 반복자의 기능을 사용하는 코드를 실행할 때 양방향 반복자를 발견하면 컴파일 에러가 발생합니다. 따라서 타입 검사 기능을 제공하지 않더라도 실질적으로는 타입을 엄격히 지켜야 합니다. 게다가 이런 에러로 발생하는 메세지는 정확하지 않습니다.

예를 들어, 랜덤 액세스 반복자를 지정해야 하는 sort() 알고리즘을 양방향 반복자만 제공하는 list에 적용하는 코드를 Visual C++ 2019로 컴파일하면, 다음과 같이 이상한 에러 메세지들이 출력됩니다.

 

반복자는 특정한 연산자만 오버로딩한다는 점에서 스마트 포인터 클래스와 구현 방식이 비슷합니다.

반복자의 기본 연산은 일반 포인터와 비슷합니다. 그래서 일반 포인터를 얼마든지 특정한 컨테이너에 대한 반복자로 쓸 수 있습니다. 실제로 vector의 반복자를 일반 포인터로 구현할 수 있습니다. 하지만 컨테이너를 사용하는 클라이언트는 이러한 구현에 대한 세부사항을 알 필요 없이 그저 반복자 인터페이스만 따라서 작성하면 됩니다.

반복자는 내부적으로 포인터로 구현되었을 수도 있고, 그렇지 않을 수도 있습니다. 따라서 포스팅에서 반복자로 접근할 수 있는 원소를 표현할 때 '참조한다' 또는 '가리킨다'라고 표현하도록 하겠습니다.

 

1.1 Getting Iterators for Containers

반복자를 지원하는 표준 라이브러리의 컨테이너 클래스는 모두 반복자 타입에 대해 public 타입 앨리어스인 iterator와 const_iterator를 제공합니다. 예를 들어, int 타입 원소에 대한 vector의 const 반복자의 타입은 std::vector<int>::const_iterator 입니다. 역방향 반복을 지원하는 컨테이너는 reverse_iterator와 const_reverse_iterator란 이름의 public 타입 앨리어스도 제공합니다. 그래서 이러한 켄테이너 반복자를 사용하는 코드는 구체적인 타입에 신경쓰지 않고 반복자를 작성할 수 있습니다.

const_iterator와 const_reverse_iterator는 컨테이너의 원소를 읽기 전용으로 접근합니다.

컨테이너는 반복자를 리턴하는 begin()과 end() 메소드를 제공합니다. begin()은 첫 번째 항목을 참조하는 반복자를 리턴하고, end()는 마지막 항목의 바로 다음 원소에 해당하는 지점을 가리키는 반복자를 리턴합니다. 다시 말해 end()는 마지막 원소를 가리키는 반복자에 operator++를 적용한 결과를 리턴합니다. begin()과 end()는 모두 첫 번째 원소는 포함하지만 마지막 원소는 포함하지 않는 half-open range를 지원합니다. 이렇게 복잡하게 구성된 이유는 빈 구간을 지원하기 위해서 입니다. 다시 말해 구간이 비어 있을 때는 begin()과 end()의 결과가 같습니다. 수학 기호로 표현하면 [being, end)로 표현됩니다.

 

추가로, 다음과 같은 메소드도 제공됩니다.

  • const 반복자를 리턴하는 cbegin()과 cend()
  • 역방향 반복자를 리턴하는 rbegin()과 rend()
  • const 역방향 반복자를 리턴하는 crbegin()과 crend()

<iterator>는 컨테이너의 특정 반복자를 검색하기 위한 다음의 비멤버 전역 함수도 제공합니다.

위 함수들은 std 네임스페이스에 정의되어 있지만, 특히 클래스와 함수 템플릿에서 제너릭 코드를 작성할 때는 다음과 같이 non-member 함수를 사용하는 것을 권장합니다.

using std::begin;
begin(...);

이렇게 하면 ADL(argument dependent lookups)를 가능하게 하여 std:: 없이도 begin()은 호출됩니다. 이러한 비멤버 함수를 사용자 타입에 맞게 사용하려면 이 사용자 타입을 std 네임스페이스에 넣거나, 비멤버 함수를 타입과 동일한 네임스페이스에 넣습니다. 후자의 경우 ADL을 활성화하므로 더 권장됩니다.

 

1.2 Iterator Traits

알고리즘을 구현하다 보면 반복자에 대한 추가 정보가 필요할 때가 있습니다. 예를 들면, 반복자가 참조하는 원소의 타입을 알아야 임시값을 저장할 수도 있고, 반복자가 양방향인지 아니면 랜덤 액세스 방식인지 알아야 할 수도 있습니다.

 

C++에서는 이러한 정보를 찾을 수 있도록 iterator_traits라는 클래스 템플릿을 제공합니다. iterator_traits 클래스 템플릿에 원하는 반복자의 타입을 지정해서 인스턴스를 만들면 value_type, difference_type, iterator_category, pointer, reference라는 다섯 가지 타입 앨리어스 중 하나에 접근할 수 있습니다.

  • value_type : The type of elements referred to
  • difference_type : A type capable of representing the distance, i.e., number of elements, between two iterators
  • iterator_category : The type of iterator: input_iterator_tag, output_iterator_tag, forward_iterator_tag, bidirectional_iterator_tag, random_access_iterator_tag, contiguous_iterator_tag(C++20)
  • pointer : The type of a pointer to an element
  • reference : The type of a reference to an element

예를 들어, 다음의 함수 템플릿은 IteratorType 타입의 반복자가 참조하는 타입으로 임시 변수를 선언합니다. 여기서 iterator_traits가 나온 문장 앞에 typename 키워드를 적었습니다. 한 개 이상의 템플릿 매개변수에 기반한 타입에 접근할 때는 반드시 typename 키워드를 명시적으로 지정해야 합니다. 이 예제에서는 템플릿 매개변수인 IteratorType으로 value_type이란 타입에 접근합니다.

#include <iterator>
template<typename IteratorType>
void iteratorTraitsTest(IteratorType it)
{
    typename std::iterator_traits<IteratorType>::value_type temp;
    temp = *it;
    std::cout << temp << std::endl;
}

이 함수는 다음과 같이 테스트할 수 있습니다.

#include <vector>
int main()
{
    std::vector v{ 5 };
    iteratorTraitsTest(cbegin(v));
}

이 코드에서 iteratorTraitsTest 함수의 temp 변수는 int 타입이며, 실행하면 5가 출력됩니다.

물론, 이 예제에서 auto 키워드를 사용하여 더욱 간결하게 표현할 수도 있습니다.

 

1.3 Examples

첫 번째 예제로 간단히 for 루프와 반복자를 사용하여 vector의 모든 원소들을 순회하고 출력하는 코드를 보겠습니다.

std::vector values{ 1,2,3,4,5,6,7,8,9,10 };
for (auto iter = cbegin(values); iter != cend(values); ++iter)
    std::cout << *iter << " ";

위 코드에서 범위의 끝을 체크하기 위해서 < 연산자를 사용하여 iter < cend(values)를 사용하려고 시도할 수도 있습니다. 하지만 이는 권장되는 방법이 아닙니다. 범위의 끝을 체크하기 위한 표준 방법은 !=를 사용하여 iter != cend(values)처럼 사용하는 것입니다. 그 이유는 != 연산자는 모든 반복자의 타입에서 동작하지만, < 연산자는 bidirectional과 forward 반복자에서 지원되지 않기 때문입니다.

 

시작과 끝으로 주어진 요소의 범위를 받는 헬퍼 메소드도 아래처럼 구현할 수 있습니다.

template<typename Iter>
void myPrint(Iter begin, Iter end)
{
    for (auto iter = begin; iter != end; ++iter)
        std::cout << *iter << " ";
}

이 메소드는 다음과 같이 사용할 수 있습니다.

myPrint(cbegin(values), cend(values));

 

두 번째로 살펴볼 예제는 주어진 범위에서 값을 찾는 myFind() 함수 템플릿입니다. 만약 값을 찾지 못한다면, 주어진 범위에서 end 반복자가 리턴됩니다. value 파라미터의 타입을 얻기위해 itertrator_traits를 사용합니다.

template<typename Iter>
auto myFind(Iter begin, Iter end,
    const typename std::iterator_traits<Iter>::value_type& value)
{
    for (auto iter = begin; iter != end; ++iter) {
        if (*iter == value)
            return iter;
    }
    return end;
}

이 함수 템플릿은 다음과 같이 사용할 수 있습니다.

std::vector values{ 11, 22, 33, 44 };
auto result{ myFind(cbegin(values), cend(values), 22) };
if (result != cend(values)) {
    std::cout << "Found value at position " << std::distance(cbegin(values), result) << std::endl;
}

여기서 std::distance() 함수는 두 반복자 사이의 거리를 리턴합니다.

 


2. Stream Iterator

표준 라이브러리에는 4개의 스트림 반복자(stream iterator)를 제공합니다. 스트림 반복자는 반복자처럼 생긴 클래스 템플릿으로서 입출력 스트림을 입출력 반복자처럼 사용할 수 있습니다. 이러한 스트림 반복자를 활용하면 입력과 출력 스트림을 다양한 표준 라이브러리 알고리즘의 입력(source)와 출력(destination)으로 활용할 수 있습니다.

표준 라이브러리에서 제공하는 스트림 반복자는 다음과 같습니다.

ostreambuf_iterator와 istreambuf_iterator도 있지만, 거의 사용되지 않아 설명은 하지 않도록 하겠습니다.

 

2.1 Output Stream Iterator

ostream_iterator는 출력 스트림 반복자로, 타입 매개변수로 원소의 타입을 받는 클래스 템플릿입니다. 생성자는 출력 스트림과 이 스트림의 원소 끝마다 붙일 string 타입의 구분자를 인수로 받습니다. ostream_iterator 클래스는 스트림에 원소를 쓸 때 operator<<를 사용합니다.

 

예를 들어 ostream_iterator는 copy() 알고리즘에 적용해서 컨테이너에 담긴 원소를 출력하는 코드를 단 한 문장으로 구현할 수 있습니다. copy()의 첫 번째 매개변수는 복제할 범위의 시작 반복자, 두 번째 매개변수는 그 범위의 끝 반복자, 세 번째 배개변수는 복제 대상이 되는 반복자입니다.

template<typename InputIter, typename OutputIter>
void myCopy(InputIter begin, InputIter end, OutputIter target)
{
    for (auto iter = begin; iter != end; ++iter) { *target = *iter; }
}

myCopy() 함수 템플릿을 사용하면 벡터의 한 원소를 다른 원소로 바로바로 복사할 수 있습니다. myCopy()의 처음 두 개의 파라미터는 복사할 범위의 begin과 end 반복자입니다. 세 번째 파라미터는 목적지의 반복자입니다. 목적지의 범위는 복사가 다 이루어질만큼 충분히 커야합니다.

int main()
{
    std::vector myVector{ 1,2,3,4,5,6,7,8,9,10 };
    std::vector<int> vectorCopy(myVector.size());
    myCopy(cbegin(myVector), cend(myVector), cbegin(vectorCopy));
}

 

이제 ostream_iterator를 사용하여 myCopy() 함수 템플릿이 벡터에 담긴 내용을 출력하도록 해보겠습니다. 다음의 코드는 myVector와 vectorCopy의 내용을 출력합니다.

myCopy(cbegin(myVector), cend(myVector), std::ostream_iterator<int>{std::cout, " " });
std::cout << std::endl;
myCopy(cbegin(vectorCopy), cend(vectorCopy), std::ostream_iterator<int>{std::cout, " " });
std::cout << std::endl;

 

2.2 Input Stream Iterator

입력 스트림 반복자 중 하나인 istream_iterator를 이용하면 입력 스트림으로부터 값을 읽는 작업을 반복자를 사용하듯이 읽을 수 있습니다. istream_iterator 역시 클래스 템플릿이므로 타입 매개변수로 원소의 타입을 받습니다. 원소를 읽을 때는 operator>>를 사용합니다. istream_iterator는 알고리즘이나 컨테이너 메소드의 원본(source)로 사용할 수 있습니다.

 

먼저 주어진 범위에서 모든 원소의 합을 계산하는 sum() 함수 템플릿이 있습니다.

template<typename InputIter>
auto sum(InputIter begin, InputIter end)
{
    auto sum{ *begin };
    for (auto iter{ ++begin }; iter != end; ++iter) { sum += *iter; }
    return sum;
}

이제 다음과 같이 istream_iterator를 사용하여 스트림이 끝날 때까지 콘솔에서 정수를 입력받는 코드를 살펴보겠습니다. 윈도우에서는 Ctrl+Z를 누른 뒤 엔터키를 누르고, 리눅스에서는 Ctrl+D를 누릅니다. 이 코드는 입력된 정수를 sum() 함수를 사용하여 모드 더합니다. 여기서 istream_iterator의 디폴트 생성자는 끝 반복자를 생성합니다.

int main()
{
    std::cout << "Enter numbers separated by whitespace.\n";
    std::cout << "Press Ctrl+Z followed by Enter to stop.\n";
    std::istream_iterator<int> numbersIter{ std::cin };
    std::istream_iterator<int> endIter;
    int result{ sum(numbersIter, endIter) };
    std::cout << "Sum: " << result << std::endl;
}

코드를 살펴보면, 모든 출력문과 변수 선언문을 제거하면 남는 것은 오직 sum() 함수를 호출하는 것입니다. 이렇게 반복자를 사용하면 콘솔에 입력받은 정수를 읽어서 모두 더하는 기능을 루프문 없이 단 한 문장으로 구현할 수 있습니다.

 


3. Iterator Adapter

표준 라이브러리에는 3가지 반복자 어댑터(iterator adaptor)를 제공합니다. 반복자 어댑터는 다른 반복자를 기반으로 만든 특수한 반복자입니다. 세 가지 반복자는 모두 <iterator> 헤더에 정의되어 있습니다. 반복자 어댑터를 직접 정의할 수도 있지만, 이번 포스팅에서 설명은 하지 않겠습니다.

 

3.1 Insert Iterator

위에서 본 myCopy() 함수 템플릿은 컨테이너에 원소를 추가하지 않고, 단순히 지정한 범위에 있는 원소를 새 원소로 교체만 합니다. 이러한 알고리즘을 조금 더 쓸모있게 만들기 위해서 표준 라이브러리는 insert_iterator, back_insert_iterator, front_insert_iterator라는 3가지 insert iterator adapoter(추가 반복자 어댑터)를 제공합니다. 이들은 컨테이너에 원소를 추가하는 기능을 제공합니다. 세 어댑터 모두 컨테이너 타입에 대한 템플릿으로 제공되며, 생성자의 인수로 실제 컨테이너에 대한 레퍼런스를 받습니다. 이러한 추가 반복자 어댑터는 myCopy()와 같은 알고리즘에서 대상 반복자로 사용하는 데 필요한 인터페이스를 제공합니다. 이 어댑터들은 컨테이너의 원소를 교체하지 않고 컨테이너에 원소를 실제로 추가하는 메소드를 호출합니다.

 

기본 버전인 insert_iterator는 컨테이너의 insert(position, element)를 호출하고, back_insert_iterator는 push_back(element)를 호출하고, front_insert_iterator는 push_front(element)를 호출합니다.

 

다음 코드는 back_insert_iterator를 myCopy() 에 사용하여 vectorOne으로부터 모든 원소를 vectorTwo로 복사합니다. 처음 vectorTwo의 크기를 미리 할당하지 않아도 됩니다.

int main()
{
    std::vector vectorOne{ 1,2,3,4,5,6,7,8,9,10 };
    std::vector<int> vectorTwo;
    std::back_insert_iterator<std::vector<int>> inserter{ vectorTwo };
    myCopy(cbegin(vectorOne), cend(vectorOne), inserter);
    myCopy(cbegin(vectorTwo), cend(vectorTwo), std::ostream_iterator<int>{std::cout, " " });
}

 

또한, 유틸리티 함수인 std::back_inserter()로도 back_insert_iterator를 생성할 수 있습니다. 방금 코드에서 inserter 변수를 정의하는 문장을 지우고, myCopy()를 호출하는 문장을 다음과 같이 수정합니다. 결과는 위와 동일합니다.

int main()
{
    std::vector vectorOne{ 1,2,3,4,5,6,7,8,9,10 };
    std::vector<int> vectorTwo;
    myCopy(cbegin(vectorOne), cend(vectorOne), std::back_inserter(vectorTwo));
    myCopy(cbegin(vectorTwo), cend(vectorTwo), std::ostream_iterator<int>{std::cout, " " });
}

C++17부터 추가된 CTAD(class template argument deduction)을 통해 다음과 같이 작성해도 됩니다.

int main()
{
    std::vector vectorOne{ 1,2,3,4,5,6,7,8,9,10 };
    std::vector<int> vectorTwo;
    myCopy(cbegin(vectorOne), cend(vectorOne), std::back_insert_iterator{ vectorTwo });
    myCopy(cbegin(vectorTwo), cend(vectorTwo), std::ostream_iterator<int>{std::cout, " " });
}

 

front_insert_iterator와 insert_iterator는 동작 방식이 비슷합니다. 단, insert_iterator는 생성자에서 초기 반복자 위치를 인수로 받고, 이를 처음 insert(position, element)를 호출할 때 전달한다는 점이 다릅니다. 그 후 반복자 위치는 insert()를 호출한 결과로 알아낼 수 있습니다.

 

insert_iterator를 이용하면 연관 컨테이너(associative container)를 modifying algorithms의 대상(destination)으로 사용할 수 있습니다. 연관 컨테이너는 반복하는 동안 원소를 수정할 수 없다는 한계가 있습니다. 하지만 insert_iterator를 사용하면 원소를 추가하게 만들 수 있습니다. 실제로 연관 컨테이너는 반복자 위치를 인수로 받는 insert() 메소드를 제공하고 있습니다. 이 위치는 단순히 참고용으로만 사용하기 때문에 무시하기도 합니다. 연관 컨테이너에 대해 insert_iterator를 사용할 컨테이너의 begin()과 end() 반복자를 힌트로 사용하도록 전달할 수 있습니다. 그러면 insert_iterator는 insert() 호출이 끝날 때마다 방금 추가한 원소의 바로 다음 지점을 가리키도록 반복자 힌트를 수정합니다.

 

앞서 본 예제를 vector 대신 set을 대상 컨테이너로 사용하도록 수정하면 다음과 같습니다.

int main()
{
    std::vector vectorOne{ 1,2,3,4,5,6,7,8,9,10 };
    std::set<int> setOne;

    std::insert_iterator<std::set<int>> inserter{ setOne, begin(setOne) };
    myCopy(cbegin(vectorOne), cend(vectorOne), inserter);
    myCopy(cbegin(setOne), cend(setOne), std::ostream_iterator<int>{std::cout, " "});
}

back_insert_iterator와 비슷하게, std::inserter() 유틸리티 함수를 사용하여 insert_iterator를 생성할 수 있습니다.

int main()
{
    std::vector vectorOne{ 1,2,3,4,5,6,7,8,9,10 };
    std::set<int> setOne;

    myCopy(cbegin(vectorOne), cend(vectorOne), std::inserter(setOne, begin(setOne)));
    myCopy(cbegin(setOne), cend(setOne), std::ostream_iterator<int>{std::cout, " "});
}

물론, CTAD도 사용 가능합니다.

int main()
{
    std::vector vectorOne{ 1,2,3,4,5,6,7,8,9,10 };
    std::set<int> setOne;

    myCopy(cbegin(vectorOne), cend(vectorOne), std::insert_iterator{ setOne, begin(setOne) });
    myCopy(cbegin(setOne), cend(setOne), std::ostream_iterator<int>{std::cout, " "});
}

 


3.2 Reverse Iterator

표준 라이브러리는 양방향 또는 랜덤 액세스 반복자를 통해 역방향으로 탐색하게 해주는 std::reverse_iterator 클래스 템플릿을 제공합니다. 표준 라이브러리에서 제공하는 컨테이너 중에서 역방향 탐색을 지원하는 것들은 모두 reverse_iterator 타입 앨리어스와 rbegin() 및 rend() 메소드를 지원합니다. 참고로 forward_list와 비정렬 연관 컨테이너를 제외한 나머지 표준 컨테이너가 모두 여기에 해당합니다. reverse_iterator 타입 앨리어스가 가리키는 타입은 std::reverse_iterator<T>이며, T는 컨테이너의 iterator 타입 앨리어스와 같습니다. rbegin()은 컨테이너의 마지막 원소를 가리키는 reverse_iterator를 리턴하고, rend() 메소드는 컨테이너의 첫 번째 원소 바로 앞 지점을 가리키는 reverse_iterator를 리턴합니다. reverse_iterator에 operator++을 적용하면 내부 컨테이너 반복자에 operator--가 적용되고, operator--를 적용하면 내부 컨테이너 반복자에 operator++이 적용됩니다.

예를 들어, 컨테이너의 시작부터 끝까지 정방향으로 반복하는 코드를 다음과 같이 작성할 수 있습니다.

for (auto iter = begin(collection); iter != end(collection); ++iter) {}

컨테이너의 끝부터 시작까지 반복하려면 다음과 같이 reverse_iteration을 기준으로 rbegin()과 rend()를 호출하도록 작성하면 됩니다. 여기서도 정방향 반복문과 마찬가지로 ++iter를 호출합니다.

for (auto iter = rbegin(collection); iter != rend(collection); ++iter) {}

 

std::reverse_iterator는 주로 역방향 실행을 지원하지 않는 표준 라이브러리 알고리즘에서 사용합니다. 예를 들어 위에서 살펴본 myFind() 함수는 시퀀스의 첫 번째 원소를 탐색합니다. 이때 myFind()에 reverse_iterator를 사용하면 마지막 원소를 탐색할 수 있습니다. 이때 리턴되는 값도 reverse_iterator가 됩니다. 만약 reverse_iterator에서 (원본)iterator를 구하고 싶다면 reverse_iterator의 base() 메소드를 호출하면 됩니다. 이때 reverse_iterator의 구현 방식으로 인해 base()가 리턴한 iterator는 reverse_iterator가 참조하는 원소의 바로 다음 지점을 가리킵니다. 따라서 참조하는 원소를 구하려면 반드시 1을 빼주어야 합니다.

 

다음 코드는 myFind()에 reverse_iterator를 사용한 예제입니다.

int main()
{
    std::vector myVector{ 11, 22, 33, 22, 11 };
    auto it1{ myFind(begin(myVector), end(myVector), 22) };
    auto it2{ myFind(rbegin(myVector), rend(myVector), 22) };
    if (it1 != end(myVector) && it2 != rend(myVector)) {
        std::cout << "Found at position " << std::distance(begin(myVector), it1)
            << " going forward." << std::endl;
        std::cout << "Found at position " << std::distance(begin(myVector), --it2.base())
            << " going backward." << std::endl;
    }
    else {
        std::cout << "Failed to find." << std::endl;
    }
}

결과는 다음과 같습니다.

 

 


4. Move Iterator

대입 연산이나 복사 생성 뒤에 원복 객체가 어짜피 제거될 거라면 쓸데없는 복사 연산을 하지 않도록 move semantic(이동 의미론)을 적용하는 것이 좋습니다. C++은 반복자 어댑터의 이동 의미론 버전인 std::move_iterator도 제공합니다. move_iterator의 역참조 연산자는 값을 자동으로 우측값 레퍼런스로 변환해줍니다. 그래서 값을 복사하지 않고도 대상 지점으로 이동시킬 수 있습니다. 단, 이동 의미론을 적용하기 전에 반드시 해당 객체가 이를 지원하는지 확인해야 합니다.

아래에 나온 MoveableClass 클래스는 이동 의미론을 지원하도록 정의하였습니다.

class MoveableClass
{
public:
    MoveableClass() {
        std::cout << "Default constructor" << std::endl;
    }
    MoveableClass(const MoveableClass& /* src */) {
        std::cout << "Copy constructor" << std::endl;
    }
    MoveableClass(MoveableClass&& /* src */) noexcept {
        std::cout << "Move constructor" << std::endl;
    }
    MoveableClass& operator=(const MoveableClass& /* rhs */) {
        std::cout << "Copy assignment operator" << std::endl;
        return *this;
    }
    MoveableClass& operator=(MoveableClass&& /* rhs */) noexcept {
        std::cout << "Move assignment operator" << std::endl;
        return *this;
    }
};

이 클래스에서 생성자와 대입 연산자는 특별히 하는 일은 없습니다. 그저 호출 대상을 쉽게 볼 수 있도록 콘솔에 메세지를 출력만 합니다. 이제 vector에 MoveableClass 인스턴스를 몇 개 넣어보도록 하겠습니다.

int main()
{
    std::vector<MoveableClass> vecSource;
    MoveableClass mc;
    vecSource.push_back(mc);
    vecSource.push_back(mc);
}

출력은 다음과 같습니다.

먼저 코드의 line 3은 디폴트 생성자를 사용해서 MoveableClass 인스턴스를 생성합니다. 첫 번째 push_back()이 호출되면 복사 생성자를 불러서 mc를 vector에 복사합니다. 그러면 이 벡터에 mc에 대한 첫 번째 복사본만큼 공간이 생깁니다. (참고로 vector의 초기 용량과 증가 방식이 표준에 명확히 정의되어 있지 않기 때문에 이 출력 결과는 컴파일마다 다를 수 있습니다.)

두 번째 push_back()이 호출되면 두 번째로 추가할 원소에 대한 공간을 확보하도록 vector의 크기를 조정합니다. 이때 크기가 변경된 새 vector에 기존 vector의 원소를 이동시키기 위해 이동 생성자가 호출됩니다. 그리고 나서 mc를 vector에 두 번째로 복사하도록 복사 생성자가 호출됩니다. 이동 생성자와 복사 생성자의 호출 순서는 일정하지 않습니다. 따라서 결과의 3,4번째 문장이 출력되는 순서는 얼마든지 바뀔 수 있습니다.

 

vecSource의 원소를 복사해서 vecOne이란 이름으로 vector를 새로 생성하려면 다음과 같이 작성합니다.

std::vector<MoveableClass> vecOne(cbegin(vecSource), cend(vecSource));

이 코드에서 move_iterator를 사용하지 않으면 vecSource에 담긴 원소마다 한 번씩 복사 생성자가 호출됩니다. 따라서 복사 생성자가 다음과 같이 두 번 호출됩니다.(마지막 두줄)

std::make_move_iterator()를 사용해서 move_iterator를 생성하면 MoveableClass의 복사 생성자 대신 이동 생성자가 호출됩니다.

std::vector<MoveableClass> vecTwo(std::make_move_iterator(begin(vecSource)), std::make_move_iterator(end(vecSource)));

또한 CTAD를 적용하여 move_iterator를 간결하게 사용할 수 있습니다.

std::vector<MoveableClass> vecTwo(std::move_iterator{ begin(vecSource) },
                                  std::move_iterator{ end(vecSource) });

 

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

[C++] Function Object (함수 객체)  (0) 2022.02.22
[C++] Function Pointer (함수 포인터)  (0) 2022.02.22
[C++] 연산자 오버로딩 (2)  (0) 2022.02.20
[C++] 연산자 오버로딩 (1)  (0) 2022.02.20
[C++] I/O 스트림  (0) 2022.02.19

댓글