'Iterator'에 해당되는 글 2건

  1. 2008/03/06 귀차니스트 std::copy - ostream_iterator 사용법의 이유
  2. 2008/02/26 귀차니스트 STL Iterator 에서의 후위, 전위 연산 차이

  가장 맨 처음 어떤 STL Container에 들어있는 값들을 출력을 하려고 하면 흔히 사용하는 방법이 해당 Container에 속해있는 Iterator로 전진반복을 하면서 출력하는 것입니다. 보통 아래와 같은 코드가 되죠.

IterTest.cpp (Language : cpp)
  1. #include <iostream>
  2. #include <algorithm>
  3. #include <vector>
  4. int main( int argc, char **argv )
  5. {
  6.     std::vector< int > abc;
  7.     abc.push_back( 1 );
  8.     abc.push_back( 2 );
  9.     abc.push_back( 3 );
  10.     abc.push_back( 4 );
  11.     abc.push_back( 5 );
  12.     for( std::vector< int >::const_iterator Iter = abc.begin(); Iter != abc.end(); ++Iter ) {
  13.         std::cout << *Iter << std::endl;
  14.     }
  15.     return 0;
  16. }

  그런데 알고보면 이게 오히려 가독성을 떨어트릴 수도 있습니다. 실제로 for 구문에 대하여 모든 것을 인식해야 하고 조건, 증가식등을 알아야 하기 때문이죠. 그에 반해 좋은 방법이 하나 존재합니다. 바로 std::copy 알고리즘을 사용하는 것인데요. 사용법은 바로 아래와 같습니다.

CopyTest.cpp (Language : cpp)
  1. #include <iostream>
  2. #include <algorithm>
  3. #include <vector>
  4. int main( int argc, char **argv )
  5. {
  6.     std::vector< int > abc;
  7.     abc.push_back( 1 );
  8.     abc.push_back( 2 );
  9.     abc.push_back( 3 );
  10.     abc.push_back( 4 );
  11.     abc.push_back( 5 );
  12.     std::copy( abc.begin(), abc.end(), std::ostream_iterator< int >( std::cout, "\r\n" ) );
  13.     return 0;
  14. }

  이 것은 std::copy가 begin 에서 부터 end 까지 ( , ] 구간을 std::ostream_iterator 함수 객체에 대하여 = assign 연산을 수행 시켜주는 알고리즘이기 때문에 가능한 것이죠. 물론 함수객체가 아닌 함수 포인터도 가능합니다. 그런데 std::ostream_iterator 가 왜 이렇게 되는지 궁금하지 않나요? iterator 헤더 안에 들어가 검색을 해보니 다음과 같은 소스 부분이 존재하더군요.

ostream_iterator.cpp (Language : cpp)
  1. template<class _Ty,
  2.     class _Elem = char,
  3.     class _Traits = char_traits<_Elem> >
  4.     class ostream_iterator
  5.         : public _Outit
  6.     {   // wrap _Ty inserts to output stream as output iterator
  7. public:
  8.     typedef _Elem char_type;
  9.     typedef _Traits traits_type;
  10.     typedef basic_ostream<_Elem, _Traits> ostream_type;
  11. #if _SECURE_SCL
  12.     typedef _Range_checked_iterator_tag _Checked_iterator_category;
  13. #endif
  14.     ostream_iterator(ostream_type& _Ostr,
  15.         const _Elem *_Delim = 0)
  16.         : _Myostr(&_Ostr), _Mydelim(_Delim)
  17.         {   // construct from output stream and delimiter
  18.         }
  19.     ostream_iterator<_Ty, _Elem, _Traits>& operator=(const _Ty& _Val)
  20.         {   // insert value into output stream, followed by delimiter
  21.         *_Myostr << _Val;
  22.         if (_Mydelim != 0)
  23.             *_Myostr << _Mydelim;
  24. .
  25. .
  26. .
  27. 생략

  바로 선언시 전달 받은 ostream_type을 함수객체의 지역성에 의하여 보관하고 있는 상황에서 해당 객체에 대한 = operator에 의한 Assign 문장이 실행되면 *Myostr 에 << 연산으로 값을 넣게 되는 것입니다. 이 것은 미리 basic_ostream 객체( 예를 들면 cout )에서 미리 정의된 객체에 대하여 << 연산자가 미리 정의 되어있기 때문에 가능한 것이죠. 만약 미리 정의 되지 않았다면 << 연산에 대하여 컴파일 에러가 발생합니다. 이 것 뿐만이 아니라 iterator 종류는 무척 많습니다.

back_insert_iterator.cpp (Language : cpp)
  1. #include <iostream>
  2. #include <algorithm>
  3. #include <vector>
  4. int main( int argc, char **argv )
  5. {
  6.     std::vector< int > abc;
  7.     std::vector< int > def;
  8.     abc.push_back( 1 );
  9.     abc.push_back( 2 );
  10.     abc.push_back( 3 );
  11.     abc.push_back( 4 );
  12.     abc.push_back( 5 );
  13.     std::copy( abc.begin(), abc.end(), std::back_insert_iterator< std::vector< int > >( def ) );
  14.     return 0;
  15. }

  예로써 위의 back_insert_iterator 와 같은 iterator 를 들수 있습니다. 물론 이보다는 std::insert 알고리즘이 더 좋습니다. back_insert_iterator는 해당 객체에 대하여 push_back 함수를 값을 인자로 호출해주는 것을 대행하게 되죠. 이렇게 보니 이전 for 루프에 대하여 출력하기 전 보다 훨씬 가독성도 높아지고 iterator 를 재정의 하면 새로운 부분들도 확장시킬 수 있을것 같다는 생각이 들지 않나요?? Iterator 를 해당 필요상황에 맞게 잘 사용하면 무척 좋겠다는 생각이 듭니다.

크리에이티브 커먼즈 라이센스
Creative Commons License
2008/03/06 23:55 2008/03/06 23:55

댓글을 달아 주세요

  새삼스럽게 적는 것 네요. 많은 분들이 알고 계실거라 생각하고 있지만 의외로 주위에 후위연산을 주로 사용하던 사람들은 이에 대한 차이를 모르는 경우가 많더군요. 그래서 생각 나는김에 한 번 포스팅 해봅니다.
  STL 컨테이너를 비롯하여 많은 Iterator 사용소스를 보면 후위( a++ )연산자를 사용하지 않고 전위 연산자를 주로 사용하는 것을 보실 수 있으실 겁니다. 그 것은 바로 효율성 면에서 차이가 발생하기 때문에 그렇습니다. 간단히 integer형 변수 같은 경우엔 전위 연산이나 후위 연산이나 선수를 제외한 성능에 대한 차이는 발생하지 않습니다.

Compile.cpp (Language : cpp)
  1. #include <iostream>
  2. int main( int argc, char **argv )
  3. {
  4.     int a = 0;
  5.     ++a; 혹은 a++;
  6.     return 0;
  7. }

후위연산.cpp (Language : asm)
  1. 00401196    E8 65FEFFFF CALL TestPost.main
  2. 0040119B    83C4 0C  ADD ESP,0C
  3. 0040119E    8BF0        MOV ESI,EAX
  4. 004011A0    8975 D8  MOV DWORD PTR SS:[EBP-28],ESI
  5. 004011A3    397D E4  CMP DWORD PTR SS:[EBP-1C],EDI
  6. 004011A6    75 06      JNZ SHORT TestPost.004011AE
  7. 004011A8    56    PUSH ESI
  8. 004011A9    E8 9C010000 CALL TestPost.exit

전위연산.cpp (Language : asm)
  1. 00401196    E8 65FEFFFF CALL TestPost.main
  2. 0040119B    83C4 0C  ADD ESP,0C
  3. 0040119E    8BF0        MOV ESI,EAX
  4. 004011A0    8975 D8  MOV DWORD PTR SS:[EBP-28],ESI
  5. 004011A3    397D E4  CMP DWORD PTR SS:[EBP-1C],EDI
  6. 004011A6    75 06      JNZ SHORT TestPost.004011AE
  7. 004011A8    56    PUSH ESI
  8. 004011A9    E8 9C010000 CALL TestPost.exit

  위를 보시면 간단히 Compile.cpp를 전위연산과 후위연산을 적용하여 컴파일을 해보았습니다만 생성된 실행코드에서 차이점이 발견되지 않습니다. 결국 성능상의 차이가 없다는 말이 되겠죠? 물론 CPU 아키텍쳐상으로 깊게 들어가서 ++연산이 어떠한 명령어 중간에 사용 되었을 때, 파이프라이닝에 대한 성능차이가 발생할 수 있다고 생각을 하시는 분도 계시겠지만 여기서 그 부분 까지는 생각할 것이 아니라고 생각합니다.
  그런데 왜 STL Iterator 는 성능 차이가 발생할 수 있을까요? 답을 아시는 분은 다 알고 계시겠지만 Operator Overloading 이라는 문법때문에 생기는 문제입니다. 간단히 std::vector 클래스를 살펴보면 내부 클래스로 Iterator 클래스가 존재하고 그 안에 ++, -- 연산자에 대한 오버로딩 소스가 존재합니다.

vector (Language : cpp)
  1. const_iterator& operator++()
  2. {   // preincrement
  3.     ++_Myptr;
  4.     return (*this);
  5. }
  6. const_iterator operator++(int)
  7. {   // postincrement
  8.     const_iterator _Tmp = *this;
  9.     ++*this;
  10.     return (_Tmp);
  11. }

  바로 위 소스죠. STL 벤더들 마다 구현이 약간씩 다르므로 꼭 이 것과 동일하다고 생각할 수는 없습니다. 하지만 일반적으로는 이러한 형태를 띄고 있죠. 그런데 ++ 연산자에 대하여 두 개의 오버로딩이 존재하죠?? 이 것은 전위, 후위 연산을 구별하기 위한 C++ 컴파일러의 한 가지 방법입니다. 인자에 아무것도 존재하지 않으면 전위연산, int 형 인자가 하나 전달되면 후위 연산이라고 정의되기 때문이죠.
  그런데 여기서 차이가 어떻게 생기냐?? 눈치 빠르신 분들은 코드를 먼저 보고 답을 알아내셨을지도 모르겠네요. 그 것은 객체 하나의 차이입니다. 보시다시피 전위연산은 _MyPtr을 하나 증가 시킨다음 그 에 해당하는 값을 바로 리턴하는데 반해 후위연산은 임시로 필요한 객체를 하나 생성하고 전위연산을 이용하여 증가시키는 작업 후 임시객체에 저장해놓았던 것을 리턴한다는 사실이죠. 결과적으로 보면 더 복잡해지는 것입니다. 그리고 Cost도 늘어나는셈이구요.
  사실 이 토픽은 스캇 마이어스저 Effective C++ 부분을 보면 자세히 나와있는 부분이기도 합니다. 이 것 말고도 많은 테크닉들이 존재하죠. 그 많은 테크닉을 하나하나 올바른 곳에 사용하는 방법을 아는 것이 중요하다고 생각이 듭니다.

크리에이티브 커먼즈 라이센스
Creative Commons License
2008/02/26 13:48 2008/02/26 13:48

댓글을 달아 주세요