본문 바로가기

프로그래밍/C++

스트림에서 STL 사용하기

이 글을 읽기 전에 다음 글들을 읽기를 권장합니다.

  1. 한 스트림을 다른 스트림으로 연결하기
  2. 스트림에서 이진 파일 다루기

이번에는 스트림에서 STL을 이용하는 법에 대해서 논해보겠습니다. 원래는 아래 글에 같이 묶으려했으나 글이 길어져 나누어 작성하게 되었군요.

  • 스트림은 상수가 될 수 없다.

    간단한 이야기입니다. 스트림은 그 특성상 읽을 떄나 쓸 때나 자신의 형상이 변합니다. 따라서 상수로 정의하면 스트림으로 할 수 있는게 아무 것도 없습니다. 스트림 반복자는 다른 반복자와 달리 컨테이너( 스트림 )의 내용을 바꿉니다. 이는 컨테이너와 스트림을 구분짓는 중요한 요소입니다. 앞으로의 설명에 이 내용을 잘 숙지하시기 바랍니다.

  • 스트림에서 쓸 수 있는 STL 알고리즘은 한정되어 있다.

    이 내용은 좀 중요합니다. 스트림을 쓰면 반복자의 존재로 인해 STL과 연계하기 편한 것이 사실입니다. 하지만 제대로 쓰지 않으면 오히려 독이 되는 경우가 있을 수 있습니다.

    일반적으로 단뱡향 스트림에서 반복자를 쓰는 경우는 다음이 있습니다.

    	
    void bar( istream &is )
    {
    	find( istreambuf_iterator< char >( is ),
    		  istreambuf_iterator< char >(), 'L' );
    }
    	

    내용은 단순합니다. 스트림에서 L이라는 글자가 있는가 없는가를 찾는 것이죠. 참고로, 컨테이너에서 begin()을 호출하듯이 스트림에서는 반복자 생성자에 스트림을 넣고, 컨테이너에서 end()를 호출하듯이 스트림에서는 반복자를 그냥 생성합니다.

    아, 그럼 다음과 같은 질문이 나올 수 있겠군요.

    " find()는 반환값으로 반복자를 뱉는데 어째서 그걸 받지 않나요 ? 반쪽자리 코드 아닌가요 ? "

    네. 아닙니다. 분명 컨테이너처럼 알고리즘을 사용했긴 했습니다만, 컨테이너와는 분명한 차이가 있습니다. 즉 스트림은 이미 내용이 변했습니다. 일반적으로 find()의 구현은 어떻게 되었을까요 ? 네. 컨테이너를 일일이 돌면서 해당하는 값을 찾으면 멈추겠죠. 스트림에서는 ? 네. 맞습니다. 똑같이 일일이 돌면서 해당하는 값을 찾으면 멈춥니다. 그렇다면 차이점은 ? 네. 스트림은 그 내용이 이미 바뀌었다는 겁니다. 즉 검사를 하면서 스트림 내부의 포인터는 이미 그만큼 진행을 해버렸다는 거죠.

    바꿔말하면 굳이 내가 반복자를 따로 받지 않아도 스트림 내부의 포인터는 진행된 상태로 저장되있다는 것이고, 또 그것은 하나의 스트림에 여러 반복자를 적용하는 것은 위험하다는 것입니다. 내가 하나의 스트림에 동시에 여러 반복자 처리를 하면 스트림 내부의 포인터는 어떻게 될까요 ? 모르지만, 미정의동작이라는 무시무시한 세계로 발을 들여놓는 것일지도 모릅니다. ( 문서에서는 어떻게 정의하고 있는지 찾지 못했습니다. 이런 경우에 대해서 어떻게 명시하고 있는지 확인하신 분은 꼭 일러주세요. )

    자, 어쨋거나 위의 find()코드는 올바르게 동작합니다. 하나의 스트림에 여러 반복자를 적용하는 것은 위험하다는 것도 알 수 있었습니다. 이제 다음의 코드를 봐 주시죠.

    void foo( istream &is )
    {
    	const static char DELIM[] = "nakopapa";
    	
    	search( istreambuf_iterator< char >( is ), 
    		    istreambuf_iterator< char >(),
    		    DELIM, DELIM +sizeof( DELIM ) -1 );
    }	
    	

    참고로 search의 용법은 다음과 같습니다.

    template <class ForwardIterator1, class ForwardIterator2>
    ForwardIterator1 search(ForwardIterator1 first1, ForwardIterator1 last1,
                            ForwardIterator2 first2, ForwardIterator2 last2);	
    						
    The first version of search returns 
    the first iterator i in the range [first1, last1 - (last2 - first2)) such that, 
    for every iterator j in the range [first2, last2), 
    *(i + (j - first2)) == *j.
    	

    위 코드의 의도는 해당 스트림에서 nakopapa라는 단어의 위치를 찾아라.는 겁니다. 코드에 문제가 있어 보이십니까 ? 분명히 하나의 스트림에 대해서 하나의 반복자만을 사용하고 있습니다. 이것도 find()와 마찬가지로 같을테니 굳이 반복자를 추가로 받을 필요는 없어 보입니다. 컴파일도 잘 됩니다. 자, 실행. 고고.

    못 찾습니다 ! 분명히 nakopapa는 있는데 왜 찾지는 못하고 끝나는겁니까미나ㅓ이ㅏㅓㅎㅇ

    심각한 결점이 있습니다. 제가 search의 용법에 진하게 해 놓은 부분을 다시 잘 살펴봐보십시요. 눈치 채셨습니까 ?

    네. 연속적인 내용을 검색하려면 어떻게 해야 할까요 ? 일단 n이 있는가 검색을 한 뒤에 있으면 a 검색, k 검색, o 검색, ... o가 없다. 이러면 다시 a부터 n이 있는가 검색하겠죠. 즉 O( n^2 )인 것이고 또한 이를 위해 추가적인 반복자가 필요합니다. 내부적으로 반복자를 따로 또 쓰게 되기 떄문에 이 알고리즘은 쓸 수가 없습니다.

    우리가 쓸 수 있는 알고리즘은 절대로 주어진 반복자 말고는 쓰지 말도록 해야만 합니다. 안타깝게도 이는 컴파일 단에서 검출은 불가능합니다. 즉, 그 알고리즘의 내부까지 확실히 살펴보지 않는 한 100% 의도대로 실행될 수 있다고 볼 수는 없다는 것입니다. 기준은 그것입니다. 내부적으로 해당 스트림에 대한 반복자를 따로 또 사용하게 되면 쓸 수 없다.

    현재까지 제가 파악한 확실하게 동작할 수 있다고 생각되는 알고리즘 함수들은 다음과 같습니다.

    		find(), copy(), transform(), fill(), generate()
    	

    나머지 함수들은 아직 확실하게 파악되지 않았습니다. 그래도 find(), copy(), transform()이 스트림과 연계될 수 있다는 것은 정말 크나큰 행복입니다.

    일단 제가 스트림의 STL 적용에 대해서 할 말은 여기까지입니다. 이펙티브 STL에서 이 반복자에 대한 내용을 언급해주었습니다만, 안타깝게도 그 주의사항, 한계에 대해서는 명시가 되어있지 않았습니다.

    위의 내용들만 보다보면 스트림은 이런 저런 제한이 있고 참 답답한데 왜 쓰냐 싶겠지만, STL 프로그래밍을 기본으로 깔고 설명을 하는 것이기 때문에 STL에 대해 별다른 언급을 하지 않는 것입니다. 이미 transform()과 같은 함수를 쓸 수 있다는 것 자체로 코드가 더 깔끔해질 수 있죠. find()의 경우는 크게 효용성이 잘 있는 편이 아닌데 그 이유는 get() 구문을 설명하면서 하겠습니다.