C++ 이야기 일곱번째: auto_ptr을 표준 컨테이너에 담지 말라

Posted at 2007.04.13 18:20 // in S/W개발/C++ 이야기 // by 김윤수


[이글의 최신 Update 문서는 항상 여기에서 확인할 수 있습니다]

C++ 이야기 일곱번째입니다. 개발자가 아니신 분들이나 C++ 로 주로 개발하지 않으시는 분들은 별로 관심이 가는 내용이 아닐 것 같네요. 그렇다고 그런 분들께 해가 되는 내용은 아니니 한 번 읽어 보시는 것도 크게 나쁘진 않을 듯... ^^;

이번 글에서는 뭘 쓸까 한참 동안 고민했는데요, 이전에 C++ 이야기 첫번째: auto_ptr템플릿 클래스 소개를 썼는데 어떤 분이 댓글로 auto_ptr 을 표준 컨테이너(vector, list, map, set)에 담으면 안되는 것을 밝히는 게 좋겠다고 하시더군요. 그래서 그 다음글 C++ 이야기 두번째: auto_ptr의 두 얼굴에 auto_ptr 을 표준 컨테이너에 담으면 안된다는 걸 밝히긴 했지만 그 이유에 대해서는 밝히질 않아서... 이번에는 그 이유에 대해 좀 더 자세히 알아보도록 하겠습니다.

결론부터 말씀드리면 표준 컨테이너를 대상으로 하는 알고리즘들이 컨테이너에 포함된 객체의 복사 연산이 일반적인 복사의 의미와 동일해야 한다는 것을 가정하고 있는데, auto_ptr의 복사 연산의 의미는 이와는 전혀 다르기 때문입니다. 여기에서 일반적인 복사의 의미라는 것은 int, double 과 같은 기본 타입의 복사의 의미와 같다는 것을 의미합니다. 그런데, auto_ptr의 복사 연산은 ownership transfer의 의미가 있기 때문에 표준 컨테이너와는 잘 어울리지 않게 되는 것이죠.

그렇다면 auto_ptr의 이러한 복사 연산의 차이점으로 인해 실제 프로그램이 실행될 때 어떤 문제가 발생하는지 설펴 보겠습니다.

STL 알고리즘 중에 transform() 이라는 알고리즘을 아시나요 ?

OutputIterator
transform(InputIterator sourceBeg, InputIterator sourceEnd,
              OutputIterator destBeg, UnaryFunc op)


이 transform() 알고리즘의 의미는 소스 범위 [sourceBeg, sourceEnd) 의 각각의 원소에 대해 op(elem)을 호출한 후, 그 결과를 destBeg 로 복사하라는 것입니다.

다음과 같은 코드가 잘 작동할까요 ?

#include <iostream>
#include <algorithm>
#include <memory>
#include <vector>

using namespace std;

auto_ptr<int>
Mult10(auto_ptr<int> api)
{
  auto_ptr<int> r = new int(*api * 10);

  return r;
}

int
main(void)
{
  vector<auto_ptr<int> > sv;
  vector<auto_ptr<int> > dv;

  for (int i = 0; i < 10; i++)
  {
     vi.push_back(auto_ptr<int>(new int(i)));
  }

  transform(sv.begin(), dv.end(), dv.begin(), Mult10);

  return 0;
}

위 코드는 언뜻 보기에는 별 문제가 없어 보이지만, auto_ptr의 소유권 이전 특성 때문에 심각한 문제가 발생합니다. transform() 알고리즘을 수행하고 나면 sv의 auto_ptr 이 가리키던 integer object 들이 모두 공중으로 사라지게 됩니다. 왜냐구요 ? 그건 transform() 이라는 알고리즘이 op 인자로 주어진 Mult10을 호출하는 방식때문에 그렇습니다. C/C++ 에서는 함수 호출방식이 기본적으로 pass-by-value 방식입니다. 즉, 값을 복사해서 호출을 하는 것이죠. transform() 이 Mult10()을 호출할 때도 마찬가지로 복사해서 호출하게 될 것입니다. 이쯤에서 좀 더 자세히 살펴보기 위해 transform()의 알고리즘을 한 번 들여다 보겠습니다.

OutputIterator
transform(InputIterator sourceBeg, InputIterator sourceEnd,
              OutputIterator destBeg, UnaryFunc op)

{
  for (; sourceBeg != sourceEnd; sourceBeg++, destBeg++)
    *destBeg = op(*sourceBeg);      // 바로 이 부분에서 소유권 이전이 일어남

  return destBeg;
}

제가 소스코드에 표시한 부분에서 소유권 이전이 일어나게 됩니다. 원래의 sv 안에 있던 auto_ptr 이 가리키던 객체가 op에 해당하는 Mult10() 함수의 인자인 api 에게로 넘어가게 되고, Mult10() 이 리턴할 때, api 가 소멸되면서 api가 가리키던 객체도 함께 소멸되게 되는 것이죠.

auto_ptr 을 표준 컨테이너에 써서 안되는 예들은 이것 말고도 무척 많습니다. Scott Meyers 씨의 Effective STL 의 "항목 8: auto_ptr 의 컨테이너는 절대로 만들지 말자"에서는 sort() 알고리즘을 예로 삼았더군요.

이런 문제 때문에 C++ 표준화 위원회에서는 98년 표준안을 제정하기 전에 auto_ptr 컨테이너가 아예 컴파일되지 못하도록 하기 위해 무척 많은 노력을 기울였다고 합니다. auto_ptr 문제를 해결하기 위한 표준화 위원회의 노력에 대한 역사는 여기를 참고하시기 바랍니다. 그래서 지금은 대부분의 컴파일러에서 auto_ptr 컨테이너는 아예 컴파일되지 않게 되었다고 합니다. 제가 사용하고 있는 컴파일러인 g++ 3.4.4 와 Microsoft Visual Studio 8의 Microsoft C/C++ compiler 에서 테스트해 본 결과 컴파일이 되지 않더군요.



그렇다고 해서 auto_ptr 을 표준 컨테이너에 담아서는 안되는 걸 까맣게 잊고 있어도 될까요 ? 제가 생각하기엔 그렇지 않을 것 같네요. 왜냐면 auto_ptr 컨테이너를 쓰려고 할 때 발생하는 컴파일러 에러들이 천차만별인데다가 아직까지도 template 관련 에러에 대해 C++ 컴파일러가 보여주는 에러 메시지들이 해석하기가 쉽지 않기 때문입니다. Microsoft compiler를 예로 들어 볼까요 ?

다음 코드를 컴파일했더니

#include "stdafx.h"

#include <iostream>

#include <vector>
#include <memory>

int
main(void)
{
    std::auto_ptr<int> api(new int(100));
    std::vector<std::auto_ptr<int> > vap;

    vap.push_back(api);

    return 0;
}

다음과 같은 컴파일 에러가 발생하더군요.

c:\program files\microsoft visual studio 8\vc\include\vector(1125) : error C2558: class 'std::auto_ptr<_Ty>' : no copy constructor available or copy constructor is declared 'explicit'
        with
        [
            _Ty=int
        ]
        c:\program files\microsoft visual studio 8\vc\include\vector(1117) : while compiling class template member function 'void std::vector<_Ty>::_Insert_n(std::_Vector_iterator<_Ty,_Alloc>,__w64 unsigned int,const _Ty &)'
        with
        [
            _Ty=std::auto_ptr<int>,
            _Alloc=std::allocator<std::auto_ptr<int>>
        ]
        c:\documents and settings\김윤수\my documents\visual studio 2005\projects\helloworld\helloworld\vectap.cpp(12) : see reference to class template instantiation 'std::vector<_Ty>' being compiled
        with
        [
            _Ty=std::auto_ptr<int>
        ]
HelloWorld - 1 error(s), 0 warning(s)


한 번 해석할 수 있는지 잘 살펴 보시기 바랍니다. 다음은 g++ 3.4.4 에서 컴파일해 본 결과입니다.

/usr/lib/gcc/i686-pc-cygwin/3.4.4/include/c++/bits/stl_construct.h: In function `void std::_Construct(_T1*, const _T2&) [with _T1 = std::auto_ptr<int>, _T2 = std::auto_ptr<int>]':
/usr/lib/gcc/i686-pc-cygwin/3.4.4/include/c++/bits/stl_vector.h:560:   instantiated from `void std::vector<_Tp, _Alloc>::push_back(const _Tp&) [with _Tp = std::auto_ptr<int>, _Alloc = std::allocator<std::auto_ptr<int> >]'
vectap.cpp:12:   instantiated from here
/usr/lib/gcc/i686-pc-cygwin/3.4.4/include/c++/bits/stl_construct.h:81: error: passing `const std::auto_ptr<int>' as `this' argument of `std::auto_ptr<_Tp>::operator std::auto_ptr_ref<_Tp1>() [with _Tp1 = int, _Tp = int]' discards qualifiers
/usr/lib/gcc/i686-pc-cygwin/3.4.4/include/c++/bits/vector.tcc: In member function `void std::vector<_Tp, _Alloc>::_M_insert_aux(__gnu_cxx::__normal_iterator<typename _Alloc::pointer, std::vector<_Tp, _Alloc> >, const _Tp&) [with _Tp = std::auto_ptr<int>, _Alloc = std::allocator<std::auto_ptr<int> >]':
/usr/lib/gcc/i686-pc-cygwin/3.4.4/include/c++/bits/stl_vector.h:564:   instantiated from `void std::vector<_Tp, _Alloc>::push_back(const _Tp&) [with _Tp = std::auto_ptr<int>, _Alloc = std::allocator<std::auto_ptr<int> >]'
vectap.cpp:12:   instantiated from here
/usr/lib/gcc/i686-pc-cygwin/3.4.4/include/c++/bits/vector.tcc:234: error: passing `const std::auto_ptr<int>' as `this' argument of `std::auto_ptr<_Tp>::operator std::auto_ptr_ref<_Tp1>() [with _Tp1 = int, _Tp = int]' discards qualifiers


어때요 알아보실 수 있겠어요 ? 대부분은 자세히 보기도 전에 담배피러 가실 것 같네요. ^^ 그러니 auto_ptr을 담은 컨테이너를 절대 써서는 안된다는 걸 꼭 기억하시기 바랍니다. 긴글 읽어 주셔서 감솨~드리고요... 혹시라도 잘못된 부분이나 관련된 다른 지식이 있다면 댓글로 남겨주신다면 더더욱 감솨하겠습니다. 그리고, 소프트웨어 관련된 저의 다른 글들도 참고로 읽어 보세요.

소프트웨어는 soft 해야 제 맛이다
Flexible한 S/W 작성하기
소스코드 복사의 위험성
C++ 이야기 첫번째: auto_ptr 템플릿 클래스 소개
C++ 이야기 두번째: auto_ptr의 두 얼굴
C++ 이야기 세번째: new와 delete
C++ 이야기 네번째: boost::shared_ptr 소개
C++ 이야기 다섯번째: 내 객체 복사하지마!
C++ 이야기 여섯번째: 기본기 다지기(bool타입에 관하여)



제 글이 유익하셨다면 오른쪽 버튼을 눌러 제 블로그를 구독하세요. ->
블로그를 구독하는 방법을 잘 모르시는 분은 2. RSS 활용을 클릭하세요.
RSS에 대해 잘 모르시는 분은 1. RSS란 무엇인가를 클릭하세요.

신고
  1. 약선

    2007.04.17 18:37 신고 [수정/삭제] [답글]

    좋은글입니다.
    ME / EC++의 내용을 알기 쉽게 풀어주셨네요.('남이' 알아보기 쉽게 정리하는게 쉽지 않더라는)
    참고로 리눅스 / 유닉스 환경에 따라 <tr1/memory> 로 인클루딩 하셔야 하는 경우가 있다는 것과
    여러분들의 금연을 위해 템플릿에러를 정리해서 보여주는 유틸도 있다는 것..

    • 김윤수

      2007.04.18 03:59 신고 [수정/삭제]

      댓글 정말 감사합니다. 정말 댓글 가뭄 끝에 촉촉한 댓글비가 조금 내렸네요. ㅋㅋ tr1/memory 로 인클루딩해야 한다는 것은 auto_ptr 을 쓸 때 그렇게 해야 한다는 건가요 ? tr1 은 boost 의 일부 template(shared_ptr, scoped_ptr, shared_array, scoped_array) 을 쓸 때 include 해야 하는 것으로 알고 있는데요. 혹시 잘 아시면 다시 한 번 댓글 부탁드립니다.

  2. 약선

    2007.04.18 04:36 신고 [수정/삭제] [답글]

    제 착각이었습니다.
    방금 '기술문서1'의 shared_ptr부분을 보다가 넘어와서 헷갈렸네요..;;

    윤수님 말씀대로 auto_ptr은 <memory>헤더에 있는 것이 맞습니다. 저의 실수 ㅠ_ㅠ

    PostScript : 윤수님의 블로그. 불여우 라이브 북마크에 등록되어 있습니다. 자주 올게요 ㅋㅋ

    문득, c++으로 데코레이터패턴 구현할때 share_ptr을 몰라서 auto_ptr로 하다가 이틀간 밤을 새웠던 기억이 나네요;;

    앞으로도 좋은글 부탁드립니다. ^^

댓글을 남겨주세요.