한 스트림을 다른 스트림으로 연결하기

배경

C++에서 기본적으로 전역으로 제공하는 스트림은 네 개가 있습니다. 바로 cin, cout, cerr, clog 입니다. ( clog는 C++에서 처음 생긴 것으로 알고 있습니다. 긴 입력을 받아들이는 데 좋다고 합니다. ) 하지만 윈도우 프로그래밍과 같이 콘솔창을 쓰지 않는 프로그램을 짤 떄에는 저 객체들은 쓸모없어집니다. 그럴 떄에 저 스트림을 파일로 돌리면 저 객체를 다시 이용할 수 있게 됩니다. 특히 에러로그 등을 남길 때 쓸만하죠. 또한 스트림 형태와 호환되는 객체를 만들었다면 그곳으로 연결해줄 수도 있습니다. 이 글에서는 스트림을 다른 스트림과 연결하는 방법에 대해서 소개하려고 합니다.

방법

C++ 스트림에서는 다른 스트림과 엮어주는 것을 두 가지를 택하고 있습니다.

tieGet/set the tied stream
rdbuf Get/set the associated stream buffer (public member function)
두 인자가 하는 일은 비슷합니다만 그 내용이 약간 다릅니다. 일단 각 용법의 사용법을 보겠습니다.

ofstream ofs( "test.txt" );
// tie() usage
cout.tie( &ofs );
*cout.tie() << "Tie() Usage" << endl;
// rdbuf() usage
cout.rdbuf( ofs.rdbuf() );
cout << "rdbuf() Usage" << endl;

이곳에서는 setter와 getter의 함수명을 같게하는 방식을 사용하였습니다. 인자를 받으면 setter, 인자를 받지 않으면 getter입니다.

tie()

tie()의 경우는 일종의 포인터 처리나 다름이 없습니다. 스트림에다가 또 다른 스트림을 하나 연결해두는 것이죠. 대신 전역에 설정된 스트림( cout, cerr, ... )등은 ostream이니 직접 다른 ostream을 쓸 수는 없습니다. tie()는 그런 포인터 기능을 지원하기 위한 함수로 봐도 무방해 보입니다. 물론 그 ostream은 참조나 포인터가 아니라 객체 자신이기 때문에 연결된( tie()로 묶은 ) 다른 스트림을 쓰려면 *cout.tie()를 통해 연결된 다른 스트림을 가져와야만 하는 겁니다. 실제로 사용하기에는 좀 불편한 감이 있고 널리 퍼져있지도 않죠.

스트림의 구성

rdbuf()의 설명에 들어가기 전에 잠시 스트림에 대해 먼저 설명을 하겠습니다.

iostream
iostream의 구성도
출처 : cplusplus.com

위 그림이 스트림을 가장 잘 표현한 내용이 아닌가 싶습니다. 잘 보시면 스트림은 크게 두 개로 나뉘어 있습니다. 하나는 ios_base이고 또 하나는 stream_buf입니다. 각각 스트림스트림 버퍼로 통칭하겠습니다.

스트림은 기본적으로 스트림 버퍼 하나씩을 가집니다. 실제로 istream 파일까지는 스트림 버퍼를 생성자의 인자로 받습니다. 그리고 iostream 파일부터는 생성자의 인자로 스트림 버퍼를 받지는 않죠. iostream 파일에서부터는 각 스트림 개체가 streambuf를 객체로 지니게 됩니다. streambuf는 생성자의 인자가 따로 필요 없기때문에 그냥 생성이 되는 것이죠. 그래서 우리가 스트림을 통해서 무언가의 작업을 할 때에는 실제 API에 가까운 ios_base를 통해 버퍼인 streambuf를 건드리게 되는 것입니다. 실제로 streambuf를 직접 쓸 일은 잘 없을 것입니다. 따로 배우지도 않죠.

rdbuf()

이제 rdbuf()에 대해 설명할 시간이 돌아온 듯 합니다. 각 스트림은 자신과 묶인 스트림과 자신과 연계된 스트림 버퍼에 대한 포인터를 지니고 있습니다. tie()와 rdbuf()를 통해서 각각 그 포인터를 내뱉는 것이죠. 즉, rdbuf()는 자신과 연계된 스트림 버퍼의 포인터를 돌려줍니다. tie()를 이용할 떄에는 연계된 스트림을 불러내기위해 항상 ostream::tie()를 이용해야만 했지만, rdbuf()는 그 내부의 스트림버퍼를 바꿔치기한 것이기 때문에 연계된 스트림 버퍼를 사용하는데 별다른 함수가 필요하지 않는 것입니다.

헌데, fstream에서는 rdbuf()가 생각한 대로 동작하지 않습니다. 아까 위에서 언급했듯이 iostream 파일부터는 스트림 객체가 스트림 버퍼를 하나씩 지니게 됩니다. 그리고 연계된 스트림 버퍼에 그 스트림 버퍼를 연결시켜 줍니다. 그리고 rdbuf()의 setter를 통해 연계된 스트림 버퍼를 바꿀 수 있습니다만, fstream서부터는 rdbuf()의 getter를 재정의하여 무조건 자신이 지닌 파일 스트림 버퍼만을 뱉습니다. 이유는 잘 모르겠지만 내가 명시적으로 부모의 rdbuf()를 호출하지 않는 한 연계된 스트림 버퍼가 아닌 자신이 지닌 스트림 버퍼의 주소만을 뱉습니다. 자식과 부모의 행동양식이 달라져 버린 셈인데, 어쨋거나 filebufstringbuf를 스트림 버퍼로 지닌 스트림들은 rdbuf()로 연계된 포인터를 돌려도 그 자신의 함수를 호출하는 한 ( istream이나 ostream의 참조 호출 혹은 명시적 부모함수 호출 ) 연계된 스트림 버퍼가 아닌 자신의 스트림 버퍼만을 반환한다는 점을 명시하시기 바랍니다. 요컨대 rdbuf()의 getter는

  • ios::rdbuf()에서는 연계된 스트림 버퍼의 포인터를 반환하고 ( cout, cerr 등의 표준입출력 포함 )
  • fstream::rdbuf(), stringstream::rdbuf()에서는 자신의 스트림 버퍼의 포인터를 반환한다.
는 것입니다.

결론

의의는 " 표준 입출력인 전역 스트림들을 파일 스트림으로 돌려보자. "는 간단한 것이었는데, 결국 스트림 구조까지 파헤쳐가면서 공부해보게 되었습니다. 덕분에 다음에는 스트림을 쓰는 데 헷갈리지는 않을 듯 합니다. 혹자는 " 스트림은 어떤 점에서 좋냐 ? "고 묻는데, 저 역시도 딱히 스트림의 좋은 점을 설명하지는 못하겠습니다. 어찌보면 FILE *의 래퍼 클래스나 다름없다고 느껴지니까요. 하지만, 스트림은 C++에 있어서 표준으로 정의되어 있고 STL과 강력히 연계되어있다는 면에서도 이미 먹고들어간다고 생각합니다. 게다가 스트림의 잘 짜여진 구조를 보고있다보니 더욱 매력을 느낄 수밖에 없을 듯 하네요.

스트림은 꽤 크고 훌륭하덥디다.

댓글을 달아 주세요