본문 바로가기

Software/C/C++

[C++.STL]2장: 반복자(iterator)

2.1 반복자에 대한 소개

반복자(iterator)
반복자(iterator)는 포인터와 상당히 비슷하며, 컨테이너에 저장되어 있는 원소들을 참조할 때 사용한다.
반복자(iterator)란 개념은 표준 라이브러리가 제공하는 컨테이너 클래스알고리듬을 사용할 때 반드시 알아두어야 할 중요한 개념이다. 좀 추상적으로 말하자면, 반복자란 컨테이너에 저장되어 있는 모든 원소들을 전체적으로 훑어나갈 때 사용하는, 일종의 포인터와 비슷한 객체라고 할 수 있다. 알고리듬마다 각기 다른 방식으로 컨테이너를 훑어가기 때문에, 반복자에도 여러가지 종류가 있게 된다. 프로그래머는 표준 라이브러리에서 제공하는 컨테이너들에 알맞는 반복자를 생성할 수 있다. 특정 알고리듬이 어떤 컨테이너와 함께 사용할 수 있는가는 어떤 종류의 반복자를 인자로 요구하느냐에 따라 결정된다.
구간(range)
구간(range)이란 컨테이너에 담긴 값들의 시퀀스를 나타낸다. 구간은 반복자 한쌍으로 나타내며, 이 반복자들이 각각 시퀀스의 시작과 끝을 가리킨다.
포인터가 기존 프로그래밍에서 다양한 방법으로 사용되듯이, 반복자도 다양한 목적으로 사용될 수 있다. 포인터가 특정 메모리 위치를 참조하는데 사용될 수 있듯이, 반복자도 특정 값을 지정하는데 사용될 수 있으며, 연속적인 메모리 영역을 두개의 포인터로 나타내듯이, 한쌍의 반복자는 값들의 구간(range)을 설정하는데 사용될 수 있다. 그러나, 반복자의 경우에는, 설정되는 범위내의 값들이 반드시 물리적으로 연속적이어야 할 필요가 없다. 단, ① 이 두개의 반복자가 같은 컨테이너로부터 생성된 것이어야 하며, ② 두번째 반복자는 첫번째 반복자 이후에 와야한다. 그리고, 첫번째 반복자에서 두번째 반복자까지 차례로 컨테이너 내부의 원소들을 처리하게 되므로, 논리적인 연속성을 지닌다고 할 수 있겠다.
기존의 포인터는 상황에 따라, 널(Null)값을 가질 수가 있는데, 이는 아무것도 가리키지 않는다는 의미이다. 반복자도 마찬가지이다. 널 포인터를 참조하는 것이 논리적인 에러를 유발할 수 있듯이, 어떤 값도 가리키지 않는 반복자를 참조하는 것은 에러를 유발한다.
C++ 프로그램에서 메모리의 한 영역을 표시하기 위해 두개의 포인터를 사용하는 경우, 끝을 나타내는 포인터는 영역의 일부로 간주되지 않는 것이 보통이다. 예를 들어서, 크기가 10인 배열 x를 x에서 x+10까지의 메모리 영역에 걸쳐있다고 표현할 수 있는데, x+10에 위치한 원소는 배열의 일부분이 아니다. 대신, x+10이라는 포인터 값은 past-the-end라고 불리운다. 반복자도 구간을 설정하는데 있어 이와 비슷한 방법을 사용한다. 두번째 값은 구간의 일부로 인정되지 않는다. 대신에, 두번째 값은 past-the-end 원소라고 하며, 구간의 마지막 원소 다음에 위치한 원소를 지칭한다. 메모리를 가리키는 포인터의 경우처럼, 이 past-the-end 원소가 실질적인 값을 가질 수도 있고, 또 어떤 때는 past-the-end를 위해 따로 정해둔 값을 사용하기도 한다. 어쨌든, 범위의 끝을 나타내는 반복자를 참조하는 것은 피해야 한다.
end()는 끝이 아니다?
컨테이너를 다룰 때 자주 쓰이는 end()라는 멤버함수가 있는데, 이 때, end()가 가리키는 것은 컨테이너의 맨 마지막 원소가 아니라는 점에 항상 주의해야 한다. end()가 가리키고 있는 것은 맨 마지막 원소의 바로 다음번 원소이다. 따라서, 이러한 반복자를 past-the-end 반복자라 부르는 것이다. 종점을 지나쳐버린 곳을 가리키는 반복자라는 뜻이다. end() 멤버함수를 통해 얻어지는 반복자는 결과적으로 아무 의미가 없는 것을 가리키고 있는 셈이다. 따라서, 이 반복자가 가리키는 것을 참조하면 예상치 못한 오류를 초래하게 된다. 이렇듯 반복자를 다룰 때는 항상 조심해야 한다. 그리고, 참고로 아무 원소를 가지고 있지 않은 컨테이너의 begin()end()는 같아진다. 따라서, 다음과 같은 코드가 가능하다.
   bool empty(const STL_Container& container) {
      return container.begin() == container.end();
   }
기존의 포인터와 같이, 반복자를 수정하는 기본적인 연산은 증가 연산자(++)이다. 어떤 시퀀스의 마지막 원소를 가리키는 반복자에 증가 연산자를 적용시키면, 이 반복자는 past-the-end 값을 가지게 된다. 만약에 반복자 i에 유한번의 증가 연산자를 적용하여, 반복자 ij와 같아질 수 있다면, 반복자 j는 반복자 i로부터 도달가능(reachable)하다고 부른다.
반복자 구간(iterator range)
반복자 2개를 사용하여 컨테이너의 특정 구간에 속한 원소들을 나타내고자 한다면, 두번째 반복자가 첫번째 반복자로부터 도달가능(reachable)해야 한다. 그렇지 않으면, 에러가 발생한다. 이는 알고리듬이나 컨테이너 내부에서 검사하지 않기 때문에, 프로그래머가 책임져야 할 부분이다.
구간을 사용하여 컨테이너 원소들 전체를 지칭할 수 있는데, 이는 첫번째 원소를 가리키는 반복자와 끝을 가리키는 반복자로 구성한다. 또한, 구간을 사용하여 컨테이너내의 일부 시퀀스를 지칭할 수도 있다. 두개의 반복자를 사용하여 구간을 지정할 때는, 두번째 반복자는 첫번째 반복자로부터 도달가능(reachable)해야 한다. 알고리듬에서는 이 조건을 확인하지 않고 사용하기 때문에, 이 조건이 만족되지 않으면, 에러를 유발하게 된다.
이제부터는 표준 라이브러리에서 사용되는 반복자들의 종류와 반복자 관련 함수들을 설명한다.
 

2.2 반복자의 종류

표준 라이브러리에서 사용되는 반복자들은 기본적으로 다음 5가지 형태로 나눌 수 있다.
입력 반복자(input iterator) 읽기만 가능, 순방향 이동
출력 반복자(output iterator) 쓰기만 가능, 순방향 이동
순방향 반복자(forward iterator) 읽기/쓰기 모두 가능, 순방향 이동
양방향 반복자(bidirectional iterator) 읽기/쓰기 모두 가능, 순방향/역방향 이동
임의 접근 반복자(random access iterator) 읽기/쓰기 모두 가능, 임의접근
반복자는 계층적으로 분류된다. 순방향 반복자는 입력 반복자와 출력 반복자를 필요로 하는 곳이라면 언제든지 사용될 수 있으며, 양방향 반복자는 순방향 반복자가 올 수 있는 자리에 올 수 있으며, 양방향 반복자를 필요로 하는 상황에서는 임의접근 반복자를 사용해도 된다.
반복자의 두번째 성질은 자신과 연결된 컨테이너의 원소들을 변경할 수 있는 능력을 가지고 있는지의 여부이다. 상수 반복자는 읽기용으로만 사용될 수 있는 반복자이며, 갱신용으로는 사용될 수 없다. 따라서, 쓰기만 가능한 출력 반복자는 절대 상수가 될 수 없으며, 반면에 읽기만 가능한 입력 반복자는 언제나 상수 반복자가 되는 것이다. 다른 반복자들은 어떤 식으로 생성되었는가에 따라 상수 반복자가 될 수도 있고, 그렇지 않을 수도 있다. 동시에 상수이면서 상수가 아닌 양방향 반복자가 있으며, 동시에 상수이면서 상수가 아닌 임의접근 반복자가 있는 등 여러가지가 있다.
아래 표는 반복자 종류별로 컨테이너에 의해서 생성되는 방식과 가능한 연산들을 정리한 것이다.
 
반복자 종류
생성되는 방식
읽기
접근
쓰기
증감
비교
입력 반복자
(input iterator)
istream_iterator =*p ->   ++ == !=
출력 반복자
(output iterator)
ostream_iterator 
inserter 
front_inserter 
back_inserter
    *p= ++  
순방향 반복자
(forward iterator)
  =*p -> *p= ++ == !=
양방향 반복자 
(bidirectional iterator) 
list
setmultiset
mapmultimap
=*p -> *p= ++ -- == !=
임의접근 반복자
(random access iterator) 
일반 포인터
vector
deque
=*p ->
[]
*p= ++ --
+  -
+= -=
== !=
<  >
<= >=

2.2.1 입력 반복자(input iterator)

입력 반복자(input iterator)는 가장 단순한 형태의 반복자이다. 입력 반복자의 성질을 이해하기 위해, 예제 프로그램을 보자. find()라는 generic 알고리듬은 순차 검색을 하여 컨테이너에 포함된 특정 값을 찾아내는 일을 한다. 컨테이너의 내용은 firstlast의 두 반복자로 지정된다. firstlast와 같지 않는 동안에는, first가 가리키는 값을 찾고자 하는 값과 비교한다. 만약에 같다면, 찾아낸 원소를 가리키는 반복자를 리턴한다. 같지 않으면, first 반복자를 증가시키고, 루프 주기를 한번 돌게 된다. range 전체를 모두 검사했는데도 원하는 값을 찾지 못하면, find()는 end-of-range 반복자를 반환한다.
    template <class InputIterator, class T>
    InputIterator find (InputIterator first, InputIterator last, const T& value)
    {
       while (first != last && *first != value) 
          ++first;
       return first;
    }
이 알고리듬은 입력 반복자가 갖춰야 할 세가지 요구사항을 설명하고 있다.
  • 반복자는 다른 반복자와의 상등여부를 비교할 수 있어야 한다. 같은 위치를 가리키면 같은 것이고, 그렇지 않다면 다른 것이다.
  • 반복자는 * 연산자를 사용하여 반복자가 가리키는 값을 얻을 수 있어야 한다.
  • 반복자는 ++ 연산자를 사용하여 다음 원소를 가리킬 수 있도록 증가될 수 있어야 한다.
입력 반복자에는 다음 세가지 종류가 있다.
일반 포인터. 일반적으로 사용되는 그냥 평범한 포인터들은 입력 반복자로 사용될 수가 있다. 포인터를 참조하고 증가시킬 수 있으므로, 임의의 값을 접근할 수 있으며, 따라서 포인터들은 입력 반복자나 출력 반복자로 사용할 수 있다. 예를 들어, 다음은 정수 배열안에서 7을 찾는 코드이다.
일반 포인터와 반복자
C++ 포인터임의접근 반복자와 기능면에서 동일하기 때문에, 표준 라이브러리의 generic 알고리듬은 표준 라이브러리에서 제공되는 컨테이너뿐만 아니라 C++ 배열에도 사용될 수 있다.
    int data[100];
       ...
    int *where = find(data, data + 100, 7);
상수 포인터는 선언시에 키워드 const를 붙이면 되며, 상수 포인터에 의해 참조되는 배열의 원소는 수정이 불가능하다.
    const int *first = data;
    const int *last = data + 100;
    // can't modify location returned by the following
    const int *where = find(first, last, 7);
컨테이너 반복자(container iterator). 표준 라이브러리가 제공하는 다양한 컨테이너로부터 생성된 반복자들은 모두 입력 반복자가 갖추어야 할 조건을 만족한다. 콜렉션의 첫번째 원소는 begin()이라는 멤버 함수를 통해서 얻을 수 있으며, past-the-end를 가리키는 반복자는 end()라는 멤버 함수를 통해서 얻을 수 있다. 예를 들어, 다음은 정수 리스트에서 7을 찾는 코드이다.
    list<int>::iterator where = find(aList.begin(), aList.end(), 7);
반복자를 지원하는 각각의 컨테이너들은 클래스 선언안에 'iterator'라는 이름을 가진 타입을 제공해야 한다. 이것을 사용함으로써, 반복자를 일관된 형태로 선언할 수 있다. 만약에 접근하고자 하는 컨테이너가 상수이거나, const_iterator를 사용하여 선언하면, 그 반복자는 상수 반복자가 된다.
입력 스트림 반복자(input stream iterator). 표준 라이브러리는 입력 반복자를 사용하여 입력 스트림에 대해 작업을 할 수 있는 방법을 제공한다. 이는 istream_iterator 클래스를 통해 제공되는데 2.3.1절에서 더 자세히 다루도록 한다.

2.2.2 출력 반복자(output iterator)

출력 반복자(output iterator)는 입력 반복자와는 반대되는 기능을 가진다. 출력 반복자는 시퀀스에 값을 대입하는데 사용될 수 있지만, 값을 접근하는데는 사용될 수 없다. 예를 들어, 한 시퀀스에서 다른 시퀀스로 값들을 복사하는 generic 알고리듬에서 출력 반복자를 사용할 수 있다.
    template <class InputIterator, class OutputIterator>
    OutputIterator copy(InputIterator first, InputIterator last, OutputIterator result) 
    {
        while (first != last) 
           *result++ = *first++;
        return result;
    }
병렬 시퀀스
많은 수의 generic 알고리듬들이 두개의 병렬 시퀀스를 다루는 것들이다. 대부분의 경우, 두번째 시퀀스는 반복자 한쌍 대신 시작 반복자 한개만을 사용하여 표시된다. 이런 경우에는 두번째 시퀀스가 적어도 첫번째 시퀀스만큼의 원소 갯수를 가지고 있다고 가정하고 있다. 다만, 이 조건은 프로그래머가 책임져야 할 부분이다.
여기서는 두개의 range가 사용되었는데, 하나는 한쌍의 반복자로 표현된 소스 range이고, 하나는 목적지 range이다. 그러나, 목적지 range는 한개의 반복자로만 표현이 되어있다. 이는 목적지가 소스쪽의 모든 값들을 수용할 수 있을만큼 충분히 크다고 가정하는 것이다. 따라서, 만약에 목적지가 충분한 공간을 확보하고 있지 않다면, 에러가 발생할 수 있다.
이 알고리듬이 설명하고 있듯이, 출력 반복자는 자신이 가리키는 원소들을 수정할 수 있다. 출력 반복자는 * 연산자를 사용할 때, 대입연산의 대상이 되는 방식으로만 사용될 수 있으며, 자신이 가리키는 원소들을 리턴하거나 접근하기 위해 사용될 수 없다.
앞에서 말했듯이, 표준 라이브러리가 제공하는 컨테이너가 생성한 반복자뿐만 아니라, 일반적인 포인터도 출력 반복자의 한 예가 될 수 있다.(일반 포인터는 임의접근 반복자이므로, 동시에 출력 반복자가 될 수 있다.) 그러므로, 예를 들어, 다음 코드는 평범한 C 형태의 배열에서 표준 라이브러리 vector로 원소들을 복사한다.
    int data[100];
    vector<int> newdata(100);
       ...
    copy (data, data+100, newdata.begin());
istream_iterator가 입력 반복자 방식을 사용하여 입력 스트림을 다루는 방법을 제공하는 것과 마찬가지로, 표준 라이브러리는 ostream_iterator 타입을 제공한다. 이는 반복자 방식으로 출력 스트림에 값들을 쓰도록 해준다. 이에 관해서는 2.3.2절에서 더 자세히 다루도록 한다.
출력 반복자의 또 다른 형태는 '삽입 반복자'이다. 삽입 반복자는 출력 반복자의 참조/대입하고 증가시키는 연산을 컨테이너에 대한 삽입연산으로 바꾼다. 이렇게 함으로써 copy()와 같은 연산들을 list와 set과 같은 가변길이 컨테이너들과 함께 사용할 수 있게 된다. 삽입 반복자에 관해서는 2.4절에서 보다 자세히 다룬다.

2.2.3 순방향 반복자(forward iterator)

순방향 반복자(forward iterator)는 입력 반복자와 출력 반복자의 특징을 결합한 것이다. 값을 접근하고 갱신하는 것이 가능하게 된 것이다. 순방향 반복자를 사용하는 함수로 replace() generic 알고리듬이 있는데, 이는 원하는 값을 찾아 다른 값으로 대치하는 작업을 한다. 이 알고리듬은 다음과 같이 작성될 수 있다.
    template <class ForwardIterator, class T>
    void replace (ForwardIterator first, ForwardIterator last, 
                  const T& old_value, const T& new_value)
    {
        while (first != last) {
           if (*first == old_value) 
              *first = new_value;
           ++first;
        }
    }
일반적인 포인터도 순방향 반복자로 사용될 수 있다. 다음 예는 정수 vector에서 나타나는 모든 7을 11로 바꾼다.
    replace(aVec.begin(), aVec.end(), 7, 11);

2.2.4 양방향 반복자(bidirectional iterator)

양방향 반복자(bidirectional iterator)는 순방향 반복자와 상당히 비슷하다. 다만, 양방향 반복자는 감소 연산자(--)를 지원하므로, 컨테이너내의 원소들을 순방향이나 역방향으로 이동하는 것이 허용된다는 점에서 차이가 난다. 예를 들어, 컨테이너 내의 값들의 순서를 뒤집어 새로운 컨테이너로 옮기는 함수에서는 양방향 반복자를 사용한다.
    template <class BidirectionalIterator, class OutputIterator>
    OutputIterator reverse_copy (BidirectionalIterator first,
                                 BidirectionalIterator last,
                                 OutputIterator result) 
    {
        while (first != last) 
        *result++ = *--last;
        return result;
    }
last 인자가 처음 가리키는 값은 콜렉션의 일부가 아니므로, 먼저 감소시킨후 (--last) 값을 참조(*)해야 한다.
다음 reverse_copy() 함수는 연결 리스트의 값들을 뒤집어, 그 결과를 vector로 옮긴다.
    list<int> aList;
     ....
    vector<int> aVec(aList.size());
    reverse_copy(aList.begin(), aList.end(), aVec.begin());

2.2.5 임의접근 반복자(random access iterator)

어떤 알고리듬은 단순히 앞뒤 방향으로 값들을 접근하는 능력 이상의 기능을 필요로 한다. 임의접근 반복자(random access iterator)는 첨자에 의한 접근이 가능하고, 다른 반복자와의 차(이 경우에는 두 반복자 사이에 존재하는 원소의 수를 얻을 수 있다)를 구할 수 있고, 산술연산을 통해 값을 수정할 수 있는 등, 기존의 일반 포인터가 했던 모든 것을 할 수 있다.
일반 포인터를 사용할 때, 산술 연산은 메모리와 관련된다. 즉, x+10x부터 10개 원소 이후의 메모리위치를 나타낸다. 반복자의 경우에도 논리적인 의미는 그대로 유지된다. 그러나, 물리적 주소는 달라지게 된다.
임의접근 반복자를 사용하는 알고리듬에는 정렬과 이진검색과 같은 연산등을 포함한다. 예를 들어, 다음 알고리듬은 컨테이너의 원소들을 임의로 섞는다. 이 알고리듬은 표준 라이브러리가 제공하는 random_shuffle()보다 좀 단순하긴 하지만, random_shuffle()과 비슷하다.
    template <class RandomAccessIterator>
    void mixup(RandomAccessIterator first, RandomAccessIterator last)
    {
       while (first < last) {
          iter_swap(first, first + randomInteger(last - first));
          ++first;
       }
    }
randomInteger()
여기서 설명한 randomInteger() 함수는 이후에 나오는 예제 프로그램에서 자주 사용될 것이다.
이 프로그램은 first 반복자가 last가 될 때까지 루프를 반복한다. 오직 임의 접근 반복자만이 관계 연산자를 이용하여 서로 비교가 가능하며, 이외의 다른 반복자들은 오직 상등 여부만을 판단할 수 있을 뿐이다. 매번 루프를 돌 때마다 last - first라는 수식을 통해 양 끝 사이에 속하는 원소들의 갯수를 얻게되며, randomInteger() 함수를 사용하여 0과 인자값사이에 속하는 난수를 얻을 수 있다. 이 함수는 표준 난수 발생기를 사용하여, 다음과 같이 작성할 수 있다.
    // n보다 작고 0보다 크거나 작은 정수형 난수를 리턴
    unsigned int randomInteger(unsigned int n)
    {
       return rand() % n;
    }
이 난수값은 first 반복자 위치에 더해져서, 컨테이너 내에서 임의의 위치를 가리키게 된다. 그리고, 이 반복자가 가리키는 값을 first가 가리키는 원소와 맞바꾸게 된다.

2.2.6 역 반복자(reverse iterator)

반복자는 자신과 연결된 컨테이너상에서의 순서를 지니고 있다. vector와 map에서는 첨자를 증가시키면서 순서가 정해지고, set에서는 크기가 증가하는 방향으로 순서가 정해지고, list에서는 값들이 첨가되는 방식에 따라 순서가 정해진다.
역 반복자(reverse iterator)는 표준 반복자가 부여받은 순서와는 반대되는 순서로 값들을 접근한다. 즉, vector나 list에서 역 반복자는 마지막 원소를 맨 먼저 접근하고, 첫번째 원소를 맨 마지막에 접근한다. set에서는 가장 큰 원소가 맨 먼저 얻어지고, 가장 작은 원소가 마지막에 접근된다. 엄격히 말해서, 역 반복자는 새로운 종류의 반복자가 아니다. 따라서, 역 양방향 반복자나, 역 임의접근 반복자등등이 있을 수 있다.
list, set, map 타입은 역 양방향 반복자를 생성하는 멤버 함수를 한쌍 가지고 제공하고 있다. rbegin()rend() 함수는 해당 컨테이너를 역순으로 훑는 반복자를 생성한다. 역 반복자에 대한 증가연산은 후퇴를 의미하고, 감소연산은 전진을 의미한다.
유사하게, vector와 deque 타입에서는 역 임의접근 반복자를 생성하는 멤버 함수를 가지고 있다.(이름은 똑같이 rbegin()rend()이다.) 증가연산 뿐만아니라, 첨자와 덧셈 연산 역시 후퇴를 의미한다.
 
 
 

2.3 스트림 반복자(stream iterator)

스트림 반복자는 반복자 연산을 이용하여 입력 또는 출력 스트림을 접근하는데 사용된다.

2.3.1 입력 스트림 반복자(input stream iterator)

스트림 반복자(Stream Iterators)
입력 스트림 반복자는 반복자 연산을 통해 입력 스트림으로부터의 read 작업을 수행하게 해주는 것이다. 이와 마찬가지로, 출력 스트림 반복자는 반복자 연산이 수행될때 출력 스트림으로 write 작업이 이루어지게 된다.
입력 반복자를 설명할 때 언급했듯이, 표준 라이브러리는 입력 스트림을 입력 반복자로 변환하는 방식을 제공한다. 이는 istream_iterator에 의해 제공된다. 선언할 때, 4개의 템플릿 인자가 필요한데, 원소의 타입, 스트림 문자의 타입, 문자 trait 타입, 그리고 원소간의 폭을 나타내는 값에 대한 타입이 필요하다. 마지막 2개는 디폴트로 char_traits<charT>ptrdiff_t를 가진다. 이 디폴트 인자는 대부분의 경우에 제대로 작동한다. istream_iterator의 생성자로 들어가는 인자는 단 하나인데, 이는 접근할 스트림을 나타낸다. 입력 스트림 반복자에 대해 증가 연산자(++)가 적용될 때마다 스트림으로부터 새로운 값을 읽어(>> 연산자 사용) 저장한다. 이 값은 참조 연산자(*)를 사용하여 얻을 수 있다. 생성자에 아무런 인자도 넣지 않고 istream_iterator에 의해 생성된 값은 입력의 끝을 나타내는 반복자 값으로 사용된다. 다음은 정수들을 지닌 화일로부터 가장 먼저 나타나는 7을 찾는 코드이다.
    istream_iterator<int> intstream(cin), eof;
    istream_iterator<int>::iterator where = 
             find(intstream, eof, 7);
입력 스트림을 위한 반복자가 가리키는 원소는 스트림에서의 다음 원소가 요청될 때까지만 유효하다. 또한, 입력 스트림 반복자는 입력 반복자이기 때문에, 원소들에 대한 접근만이 가능하며, 대입에 의한 갱신은 불가능하다. 마지막으로, 원소들은 오직 한번 순방향으로만 접근이 가능하다. 스트림의 내용을 한번 이상 읽고 싶다면, 각 pass마다 별도의 반복자를 생성해야 한다.
최종 표준에서 변경된 사항
최종 표준에서는 istream_iterator 클래스와 ostream_iterator 클래스의 두번째 템플릿 인자도 디폴트 인자로 바뀌었다. 따라서, 기존에 istream_iterator<int, char>와 같이 쓰던 것을 istream_iterator<int>와 같이 간단히 쓸 수 있게 되었다. 이 사항은 Stroustrup의 'The C++ Programming Language, 3ed' 5쇄부터 제대로 반영되어 있다. 아래 링크를 클릭해서 'pg 558'과 'pg 559'에 관련된 부분을 참고하기 바란다. 4쇄에서 5쇄로 넘어가면서 바뀐 사항들

2.3.2 출력 스트림 반복자(output stream iterator)

출력 스트림 반복자(output stream iterator) 방식은 입력 스트림 반복자와 비슷하다. 값들이 반복자로 대입될 때마다, 그 값은 >> 연산자를 사용하여 해당 출력 스트림으로 출력된다. 출력 스트림 반복자를 생성하기 위해서는 생성자의 인자로 출력 스트림을 지정해야 한다. 출력 스트림으로 쓰여지는 값들은 >> 연산자를 인식할 수 있어야 한다. 즉, 해당 값에 대해 >> 연산자가 정의되어 있어야 한다. 생성자의 생략 가능한 두번째 인자는 출력되는 값들 사이의 분리자로 사용될 문자열을 나타낸다. 다음 예는 vector의 원소값들을 표준 출력으로 복사하는 예이다. 출력되는 값들을 공백문자로 구분하고 있다.
    copy(newdata.begin(), newdata.end(), 
          ostream_iterator<int>(cout, " "));
입출력 스트림 반복자와 표준 라이브러리가 제공하는 다양한 알고리듬을 혼합하여 간단한 화일 변환 알고리듬을 만들어볼 수 있다. 다음은 표준입력으로부터 정수들이 담긴 화일을 읽어들여, 이 화일에 들어있는 7이란 값을 제거하고, 나머지는 표준 출력으로 출력하고, 출력되는 값들 사이에 개행문자를 출력하는 간단한 프로그램이다.
    int main() 
    {
       istream_iterator<int> input(cin), eof;
       ostream_iterator<int> output(cout, "n");
    
       remove_copy(input, eof, output, 7);
    }
     
     

    2.4 삽입 반복자(insert iterator)

    출력 반복자에 의해 참조되는 값에 대입을 하면, 그 위치의 내용을 덮어쓰게 된다. 예를 들어, 다음과 같이 copy() 함수를
    호출하면, 선언문에서 두번째 vector에 대한 메모리 공간이 이미 확보하였는데, 이쪽 vector에서
    다른쪽 vector로 값들을 옮기게 된다.
      vector<int> a(10);
      vector<int> b(10);
         ...
      copy(a.begin(), a.end(), b.begin());
    list와 같은 데이터구조에서도 이와 같은 방식으로 덮어쓰게 된다. 아래 c라고 이름붙은 list는 적어도 10개의 원소는 가지고
    있다고 가정할 때, 리스트의 처음 10개의 원소들은 vector a의 내용으로 바뀌게 된다.
      list<int> c;
         ...
      copy(a.begin(), a.end(), c.begin());
    list와 set과 같은 구조에서는 원소가 새로 추가되면, 동적으로 크기가 커지게 되는데, 기존의 위치에 덮어쓰는 것보다는
    삽입하는 것이 더 적절할 때가 많다. 삽입 반복자라고 불리는 일종의 어댑터를 사용하여 copy()와 같은 알고리듬을 사용할 때
    원소들을 덮어쓰지 않고, 해당 컨테이너로 삽입하게 된다. 반복자의 출력 연산은 해당 컨테이너로의 삽입 연산으로 바뀌게 된다.
    다음 예는 vector의 원소들을 공백 list에 삽입하는 코드이다.
      list<int> d;
      
      copy(a.begin(), a.end(), front_inserter(d));
    삽입 반복자에는 3가지 형태가 있는데, 이들 모두 복사 연산을 삽입 연산으로 바꾸는데 사용된다.
    위에서처럼, front_inserter() 사용하여 얻어진 반복자는 해당 컨테이너의 앞쪽에 값들을 삽입한다.
    back_inserter()를 통해 얻은 반복자는 해당 컨테이너의 뒤쪽에 값들을 삽입한다.
    두가지 형태 모두 list와 deque와 함께 사용될 수 있으나, set과 vector에는 사용될 수 없다.
    세번째 형태는 가장 일반적인 형태로 inserter가 있다. inserter는 두개의 인자를 취하는데,
    컨테이너와 컨테이너내의 반복자가 그것이다. 이 형태는 원소들을 컨테이너의 특정 위치로 복사한다.
    (list의 경우에는 원소들이 지정된 위치의 바로 앞쪽에 복사된다). 이 형태는 set과 map뿐만 아니라
    앞에서 언급한 두 형태와 같이 사용될 수 있는 모든 구조들과 같이 사용될 수 있다.
    다음은 삽입 반복자의 세가지 형태의 사용을 모두 설명하고 있다. 처음에는, 3,2,1을 빈 list의 맨 앞에 삽입한다.
    결과적으로, 1,2,3으로 구성된 list가 얻어진다. 다음에는 7,8,9를 list의 끝에 삽입한다.
    마지막으로 find() 연산을 사용하여 7의 위치를 가리키는 반복자를 찾아, 그 앞에 4,5,6을 삽입한다.
    결과는 1부터 9까지의 숫자들을 순서대로 가지는 list가 된다.
      int main()
      {
         int threeToOne[] = {3, 2, 1};
         int fourToSix[] = {4, 5, 6};
         int sevenToNine[] = {7, 8, 9};
      
         list<int> aList;
      
         // first insert into the front
         // note that each value becomes new front
         copy(threeToOne, threeToOne+3, front_inserter(aList));
      
         // then insert into the back
         copy(sevenToNine, sevenToNine+3, back_inserter(aList));
      
         // find the seven, and insert into middle
         list<int>::iterator seven = find(aList.begin(), aList.end(), 7);
         copy(fourToSix, fourToSix+3, inserter(aList, seven));
      
         // copy result to output
         copy(aList.begin(), aList.end(), 
               ostream_iterator<int,char>(cout, " "));
         cout << endl;
      }
    여기서, inserter(aList, aList.begin())front_inserter(aList)에 의해 생성된 반복자들간에 중요하고도 미묘한 차이가
    있음을 주목해야 한다.
    inserter(aList, aList.begin()) 호출하게 되면 값들을 list의 앞에 순서를 그대로 유지하여 삽입하는 반면에,
    front_inserter(aList)는 삽입되는 각각의 값들이 list에 가서는 맨앞에 삽입된다는 점이다.
    결과적으로 front_inserter(aList)는 원래 값들이 유지했던 순서가 뒤집히게 되며, inserter(aList, aList.begin())
    원래 순서를 그대로 유지한다.
     
     
     

    2.5 반복자 연산(iterator operation)

     
    표준 라이브러리는 반복자를 다루는데 사용되는 두개의 함수를 제공한다.
    advance() 함수는 하나의 반복자와 하나의 숫자값을 인자로 받아서, 반복자를 주어진 값만큼
    이동시킨다.
     
      void advance(InputIterator& iter, Distance& n);
       
    임의접근 반복자의 경우에, 위 함수는 iter + n; 과 같은 효과를 가진다.
    그러나, 위 함수는 모든 형태의 반복자에 대해 적용될 수 있다. 이동거리는 순방향 반복자의 경우에는
    반드시 양수이어야 하고, 양방향 반복자나 임의접근 반복자의 경우에는 양수나 음수이면 된다.
    위 연산은 임의접근 반복자에 대해서만 효율적(constant time)이다. 다른 경우에는 반복자에 대해
    증가 연산자(++)나 감소 연산자(--)를 호출하는 루프로 advance()를 구현한다.
    advance() 함수는 해당 반복자에 대해 연산이 유효한지에 대해서는 따로 검사하지 않는다.
    두번째 함수인 distance()는 두 반복자간의 거리를 반환한다.
     
      void distance(InputIterator first, InputIterator last, Distance& n);
       
    결과값은 레퍼런스로 넘어간 세번째 인자로 반환되며, firstlast로 옮길 때, ++연산자가 수행되는
    횟수만큼 증가한다.
    따라서, 이 함수를 호출하기 전에 세번째 인자를 통해 넘겨지는 변수가 제대로 초기화가 되었는지를
    항상 확인해야 한다.
     
    출처:http://oopsla.snu.ac.kr/~sjjung/stl/var_0565.htm