#stl auto_ptr - 조심히 사용해야 하는 스마트 포인터

3 minute read

변태적인 복사 동작을 하는 스마트 포인터다. 꼭 필요한 경우가 아니라면 레퍼런스 카운팅을 하는 스마트 포인터를 사용하자.

스마트 포인터?

In computer science, a smart pointer is an abstract data type that simulates a pointer while providing additional features, such as automatic garbage collection or bounds checking.

Smart pointer - wikipedia

스마트 포인터란 자동으로 자원 해제를 하는 등의 추가적인 동작을 하는 포인터의 추상 데이터 타입(abstract data type)이다.

소유권(ownership)을 넘겨주는 auto_ptr

TR1 이전의 STL에 있는 유일한 스마트 포인터는 auto_ptr 인데, 컨테이너에 넣지도 못하는 스마트 포인터라서 무슨 생각으로 이런 스마트 포인터를 STL에 포함했는지 고개를 갸우뚱하게 한다. 또 이게 STL 씹기를 좋아하는 사람들의 좋은 공격 목표가 되기도 한다. 열렬한 지지자인 나도 이건 정말 이해가 안 된다.

// VS2005 <memory.h>
template<class _Ty>
class auto_ptr
{ // wrap an object pointer to ensure destruction
public:
    typedef _Ty element_type;

    auto_ptr(auto_ptr<_Ty>& _Right) _THROW0()
        : _Myptr(_Right.release())
        {   // construct by assuming pointer from _Right auto_ptr
        }
};

컨테이너에 넣지 못하는 이유는 일반적인 복사 동작과는 다른 동작을 하기 때문이다. 스마트 포인터 객체를 값에 의한 전달로 넘겨주게 되면 객체 복사가 일어나게 되는데, 이때 스마트 포인터가 가진 자원도 같이 복사가 일어나는 게 아니라 자원에 대한 소유권(ownership)을 넘겨주게 된다. 다른 말로 소멸식 복사(destructive copy)라고도 하는데, A에서 B로 복사하게 되면 A가 가지고 있던 자원이 복사가 아니라 전달되게 된다.

COAP(Container Of Auto_Ptr)

STL의 알고리즘은 값에 의한 복사가 기본 동작이라서 컨테이너에 auto_ptr을 넣고 알고리즘을 돌리면 자원 소유권을 줘버리고 null을 떡하니 가진 스마트 포인터로 가득 차게 된다. 이런 실수를 하는 사람이 많아서인지 COAP(Container Of Auto_Ptr) 라는 단어도 존재하고 auto_ptr 얘기만 나오면 컨테이너에 넣지 말라고 강조하고 있다.

std::auto_ptr<int> temp = new int;

std::vector< std::auto_ptr<int> > COAP;
COAP.push_back(temp);

이렇게 COAP에 스마트 포인터 객체를 삽입하려고 하면 어떻게 될까? VS2005에서 테스트했다.

Error    1    error C2558: class 'std::auto_ptr<_Ty>' :
    no copy constructor available or copy constructor is declared 'explicit'
    c:\program files\microsoft visual studio 8\vc\include\vector    1125

오호~ 컴파일 에러를 내면서 뒤통수를 쳐주네.

To do this, the committee used a trick: auto_ptr’s copy constructor and copy assignment operator take references to non-const to the right-hand-side object. The standard containers’ single-element insert() functions take a reference to const, and hence won’t work with auto_ptrs.

Using auto_ptr Effectively - C/C++ Users Journal, 17(10), October 1999

표준 위원회에서 COAP를 막고자 컨테이너의 삽입 멤버 함수의 인자로 상수 참조(const T& arg)를 받도록 정의하는 트릭을 썼다고 한다. 복사 생성자에 상수 참조가 넘어가게 되는데, auto_ptr은 소유권 이전을 하기 때문에 복사 생성자에 상수 참조를 받지 못한다. 그래서 복사 생성자를 찾을 수 없다는 컴파일 에러를 내게 된다.

왜 이렇게 STL에 하나밖에 없는 스마트 포인터를 디자인 했을까?

// 20.4.5 Class template auto_ptr
namespace std {
    template <class Y> struct auto_ptr_ref {};
    template<class X> class auto_ptr {
    public:
        typedef X element_type;

        // 20.4.5.1 construct/copy/destroy:
        explicit auto_ptr(X* p =0) throw();
        auto_ptr(auto_ptr&) throw();
        template<class Y> auto_ptr(auto_ptr<Y>&) throw();
        auto_ptr& operator=(auto_ptr&) throw();
        template<class Y> auto_ptr& operator=(auto_ptr<Y>&) throw();
        auto_ptr& operator=(auto_ptr_ref<X> r) throw();
        ~auto_ptr() throw();

        // 20.4.5.2 members:
        X& operator*() const throw();
        X* operator->() const throw();
        X* get() const throw();
        X* release() throw();
        void reset(X* p =0) throw();

        // 20.4.5.3 conversions:
        auto_ptr(auto_ptr_ref<X>) throw();
        template<class Y> operator auto_ptr_ref<Y>() throw();
        template<class Y> operator auto_ptr<Y>() throw();
    };
}

표준 문서 ISO/IEC-14882를 보면 auto_ptr의 모든 멤버 함수는 예외(exception)를 던지지(throw) 않는다는 exception specification이 정의되어 있다. throw(type) 형식으로 어떤 타입의 예외를 던질건지 정의하는데, throw()로 정의되어 있어 어떠한 예외도 던지지 않겠다는 뜻이 된다.

예외 안정성을 가지고 오버헤드가 없는 스마트 포인터를 만드는 게 목표였나 보다. 이렇게 생각하면 왜 객체 복사나 할당이 일어날 때 소유권을 이전하는지 이해가 된다. 소유권을 이전하지 않는다면 레퍼런스 카운팅으로 자원 해제를 관리하거나 포인터 복사가 아닌 포인터를 참조하는 객체의 복사(보통 이런 동작을 Deep copy라고 한다.) 등의 동작을 해야 하는데, 이렇게 되면 모든 멤버 함수에서 예외를 던지지 않게 하는 게 불가능해진다. 참고로 tr1부터 지원하는 스마트 포인터인 shared_ptrweak_ptr 는 예외 안정성을 보장하지 않는다.

이해는 가지만 컨테이너에 넣을 수 있는 스마트 포인터도 만들어주지… 쩝.

어떻게 사용해야 잘 사용했다 소문이 날까?

auto_ptr<Temp> Allocate()
{
    return new Temp();
}

이런 스타일은 마음에 안 들지만, 경우에 따라서 함수 안에서 heap에 자원을 할당하고 던져 주는 경우가 있는데, 이럴 때는 호출하는 쪽이 쓰고 난 뒤에 해제해줘야 할지 아니면 할당하는 쪽이 관리를 따로 해서 호출하는 쪽에서 해제하지 않아도 되는지를 문서나 소스 코드를 통해 확인해야 한다. 만약 호출하는 쪽에서 책임을 져야 하는 경우면 이럴 때는 auto_ptr을 사용해 주는 게 직관적이다. 이럴 때 사용하면 딱 좋겠네.

이렇게 딱 맞는 경우가 아니면 사실 사용을 자제하는 게 좋겠다. 멤버 변수로 auto_ptr는 싱글톤이나 복사 생성자나 할당 연산자를 지원하지 않는 클래스에는 적합하나 복사를 허용하는 클래스에는 절대 사용하면 안 된다. 참 신경 써야 할게 많은 녀석이다. 스마트 포인터가 필요하면 속 편하게 레퍼런스 카운팅을 하는 걸 쓰자.

컨테이너에 넣을 수 있고 레퍼런스 카운팅을 하는 스마트 포인터가 필요하면 TR1을 지원하는 VS2008과 같은 컴파일러에서는 대신 tr1::shared_ptr를 사용하거나 TR1을 지원하지 않는 컴파일러에서는 boost::shared_ptrLoki::SmartPtr를 사용하면 된다.

PS : 내가 사용 안 해도 다른 사람이 작성한 코드를 읽을 때, 만날 수도 있는 녀석이라서 간단하게 정리를 해본다는 게 정말 길어졌다.

참고

  • Effective STL 항목 8
  • 표준 문서 ISO/IEC-14882
  • The C++ Programming Language 14.4.2