39-2-나.입력 및 출력 반복자

입력 반복자(Input Iterator)는 가장 기본적인 기능만 제공하는 반복자이다. 반복자이므로 컨테이너의 한 위치를 가리키는 것은 당연하고 반복자가 가리키는 위치의 요소를 * 연산자로 읽을 수도 있다. 읽는 것만 가능하므로 * 연산자를 사용하더라도 요소의 값을 변경하는 것은 불가능하다. 입력 반복자에 * 연산자를 적용한 결과는 우변값일 뿐이며 좌변값은 아니다. 즉, *입력 반복자 표현식은 읽기 전용이다.

 

a=*it;       // 가능

*it=a;       // 불가능

 

전위형, 후위형의 ++ 연산자를 사용하여 다음 요소로 이동하는 기능도 가지고 있으며 같은 타입의 반복자와 상등 비교도 가능하다. 검색을 하는 find 알고리즘이 요구하는 연산자가 바로 입력 반복자(InIt)인데 검색이란 지정 구간을 순회하면서 원하는 값을 가진 요소를 찾아내는 것이다. 이 동작을 하기 위해 필요한 기능은 값을 읽기 위한 * 연산자, 다음 요소로 이동하는 ++ 연산자 그리고 끝낼 시점을 결정하기 위한 != 연산자밖에 없다. find 함수의 구현 코드를 보면 과연 이 정도 기능밖에 사용되지 않는다는 것을 확인할 수 있다.

검색이란 단순히 값을 읽기만 하는 동작이므로 요소의 값을 변경하는 기능은 필요치 않으며 순차 검색을 하므로 앞으로 전진하는 기능만 있으면 된다. 이런 요구 조건을 만족하는 반복자를 입력 반복자라고 한다. 쓰기 기능은 요구되지 않는데 이는 쓰기 기능이 없어야 한다는 얘기와는 다르다. 쓰는 기능이 있어도 상관없지만 필수적으로 요구되지는 않는다는 뜻이다.

출력 반복자는 * 연산자를 사용하여 요소의 내용을 변경할 수 있는 반복자이다. 쓰기가 가능하다면 보통 읽기도 가능하지만 읽는 기능은 없어도 상관없다. 쓰기만 가능하면 출력 반복자라고 할 수 있으며 읽기 기능이 필수는 아니다. 다음 요소로 이동하는 ++연산자도 지원해야 한다. 그러나 출력은 입력과는 달리 무조건적이어서 범위 점검을 위한 ==, != 연산자는 필수가 아니다. 즉, 전진하면서 기록 가능하다는 조건만 만족하면 출력 반복자라고 할 수 있다.

 

*it=a;       // 가능

a=*it;       // 꼭 필요치 않음.

 

반복자 구간끼리 복사하는 copy 알고리즘에 입력, 출력 반복자가 동시에 나타난다. 이 함수의 구현 코드는 다음과 같을 것이다. 물론 실제 코드는 컴파일러마다 다르다.

 

OutIt copy(InIt first, InIt last, OutIt result)

{

     while (first != last) {

          *result++=*first++;

     }

     return result;

}

 

이 함수는 first ~ last 구간을 result에 복사한다. first, last는 복사할 원본이므로 오로지 읽기만 하면 되고 쓸 필요는 없으므로 InIt 타입이며 복사 목적지인 result는 오로지 출력만 하며 다시 읽을 필요는 없으므로 OutIt 타입이다. 게다가 result는 무조건 쓰기 가능한 것으로 가정하므로 범위를 점검할 필요가 없으며 따라서 출력 반복자는 비교 연산을 제공할 필요도 없다. 반면 입력 반복자는 끝 판별을 위해 != 연산을 제공해야 한다.

두 유형의 반복자는 입출력 스트림에만 적용된다. STL 컨테이너들은 모두 읽기, 쓰기가 동시에 가능하므로 이 두 유형보다 더 높은 레벨의 반복자를 지원한다. 입출력 스트림도 문자들의 집합이므로 일종의 컨테이너라고 할 수 있는데 키보드가 제공하는 반복자는 입력만 가능하다. 키보드로 들어오는 문자값은 읽을 수만 있을 뿐이지 쓰기는 가능하지도 않고 필요하지도 않다.

쓰기만 가능하고 읽기는 안되는 쓰기 전용의 컨테이너가 얼른 떠오르지 않겠지만 이런 컨테이너가 실제로 존재한다. 문자의 컨테이너인 콘솔 화면은 쓰기 전용의 장비이며 콘솔이 제공하는 스트림 출력 반복자가 바로 이런 부류이다. 더 확실한 예를 들자면 표준 출력이 프린터로 재지향되어 있을 때의 프린터를 생각해 보면 된다. 프린터로 뭔가를 보낼 수는 있지만 인쇄된 글자가 무엇인지 읽을 수 있는 방법은 전혀 없다. 화면이나 프린터는 완전한 쓰기 전용 장비이며 따라서 이 장비의 반복자는 출력용이기만 하면 된다.

입력, 출력 반복자의 일종인 입출력 스트림 반복자는 콘솔에 연결된 반복자이다. 통상 표준 입출력 객체인 cin, cout에 대해 사용하지만 임의의 스트림 객체에 대한 반복자로도 사용할 수 있다. i(o)stream_iterator 클래스로 정의되어 있으며 이 반복자를 사용하면 표준 입력(키보드)으로 입력받은 내용을 순회하면서 읽어낼 수 있고 표준 출력(모니터)으로 문자를 출력할 수도 있다. 다음 예제는 리스트의 요소들을 화면으로 출력한다.

 

: ostream_iterator

#include <iostream>

#include <list>

using namespace std;

 

void main()

{

     int ari[]={1,2,3,4,5};

     list<int> li(&ari[0],&ari[5]);

 

     ostream_iterator<int> oit(cout,",");

     copy(li.begin(),li.end(),oit);

}

 

콘솔 화면으로의 출력을 위해 ostream_iterator형의 객체 oit를 선언하는데 이 템플릿의 인수로는 출력할 타입을 지정해야 한다. 출력 대상인 리스트의 요소가 정수형이므로 int 타입에 대한 출력 스트림 클래스로 구체화했다. 생성자의 인수로는 출력 스트림 객체와 매 요소 사이에 출력될 구분자를 전달한다. 예제의 oit는 정수형을 cout, 즉 표준 출력 장치로 출력하되 매 정수값마다 쉼표를 구분자로 삽입한다.

리스트의 내용을 화면에 출력할 때는 리스트의 반복자 구간과 출력 스트림 반복자를 copy 함수로 전달하여 반복자끼리 복사하도록 했다. 리스트의 내용을 화면에 복사한다는 것은 곧 화면에 출력한다는 얘기다. copy 함수는 리스트의 반복자 구간 요소들을 순서대로 읽어 출력 스트림 반복자 oit의 위치에 복사한다. 이 코드는 아마 *oit=*first로 되어 있을텐데 출력 스트림 반복자에 대한 = 연산이 cout << 연산으로 중복 정의되어 있어 화면으로 출력되는 것이다. 실행 결과는 다음과 같다.

 

1,2,3,4,5,

 

리스트의 정수 요소들이 모두 출력되었으며 숫자들 사이에는 구분자인 쉼표가 하나씩 삽입되었다. oit 생성자의 두 번째 인수를 "\n"으로 변경하면 매 요소를 출력한 후 개행될 것이다. 컨테이너의 반복자 구간과 출력 스트림 반복자로 copy 함수를 호출하는 방법은 컨테이너를 화면에 덤프하는 가장 편리한 방법이며 다음 한 줄로 줄일 수도 있다.

 

copy(li.begin(),li.end(),ostream_iterator<int>(cout,","));

 

리스트의 전체 요소를 cout 출력 스트림 반복자로 복사하되 각 요소 사이에 쉼표를 집어 넣어라는 뜻이다. 구문이 조금 복잡하기는 하지만 얼마나 짧고 간결한가? 이걸 C 코드로 풀어 쓰려면 지역 포인터를 선언하여 선두를 가리키도록 초기화하고 루프 돌리며 매 요소를 읽어 cout으로 출력하기를 반복해야 하고 게다가 요소 사이에 쉽표까지 출력해야 하므로 족히 10여줄은 더 될 것이다.

입력 스트림 반복자를 사용하면 키보드로부터 특정 타입의 자료를 연속적으로 읽어 원하는 작업을 할 수 있다. 다음 예제는 입력 스트림 반복자로 정수를 입력받아 벡터에 저장한다. 사용자로부터 일련의 값을 입력받을 때 이 방법을 사용할 수 있다.

 

: istream_iterator

#include <iostream>

#include <vector>

#include <algorithm>

using namespace std;

 

template<typename C>

void dump(const char *desc, C c)

{

     cout.width(12);

     cout << left << desc << "==> ";

     copy(c.begin(),c.end(),ostream_iterator<typename C::value_type>(cout," "));

     cout << endl;

}

 

void main()

{

     vector<int> vi(16);

     istream_iterator<int> iit(cin);

     copy(iit,istream_iterator<int>(),vi.begin());

     dump("입력 완료 후",vi);

}

 

istream_iterator는 템플릿 인수로 입력받을 타입을 지정하며 생성자로는 입력 스트림 객체를 지정한다. 예제의 iit는 정수형을 cin으로부터 입력받는 객체로 선언되었다. istream_iterator의 디폴트 생성자는 스트림 끝을 나타내는 반복자를 생성하는데 이 반복자는 키보드 입력 종료를 의미한다.

copy 함수는 iit에 정의되어 있는 >> 연산자를 호출하여 키보드(cin)로부터 정수값을 입력받으며 이 값을 벡터의 시작 위치에 복사하기를 스트림 끝에 이를 때까지 반복할 것이다. 예제를 실행한 후 정수값들을 공백이나 개행으로 구분하여 입력하면 이 정수들이 벡터에 차례대로 복사된다. 입력을 마칠 때는 스트림의 끝을 의미하는 Ctrl+Z를 입력한다. 한 줄로 줄이면 다음처럼 쓸 수도 있다.

 

copy(istream_iterator<int>(cin),istream_iterator<int>(),vi.begin());

 

이 예제는 dump라는 템플릿 함수를 처음으로 정의하고 있는데 이 함수는 임의의 컨테이너를 전달받아 컨테이너의 모든 요소를 공백으로 구분하여 화면에 출력한다. 컨테이너 내용앞에 간단한 문자열을 하나 출력하며 다 출력한 후 개행까지 처리하므로 예제 확인용으로 아주 훌륭하다. 화면 출력을 위해 출력 스트림 반복자를 사용하고 있는데 반복자로 직접 출력할 수도 있다.

 

for (typename C::iterator it=c.begin();it!=c.end();it++) cout << *it << ' ';

 

앞으로 컨테이너와 알고리즘 실습을 위해 컨테이너를 변경하고 확인할 일이 아주 많으므로 이후부터 컨테이너의 내용을 출력할 때는 이 함수를 활용하기로 한다. 임의 타입의 임의 컨테이너를 지원할 수 있도록 아주 일반적으로 작성했으므로 디버깅용으로도 훌륭하게 활용할만하다.

입출력 스트림 반복자에는 이 외에도 좀 더 저수준의 i(o)strembuf_iterator 클래스도 있는데 버퍼를 직접 조작하며 문자 단위로 입출력을 수행하므로 성능이 훨씬 더 좋다. 이 클래스들도 나름대로 실용성이 있기는 하지만 여기까지만 알아 두면 충분하다. 어차피 콘솔 환경에서 프로그래밍하는 시대는 지났으며 iostream은 아직까지도 표준대로 구현되지 않은 컴파일러가 많아 이식성에도 불리하므로 애써 연구해볼 가치가 떨어진다.