본문 바로가기

Programming/MFC

boost::shared_ptr 살펴보기

출처 카페 > Effective C++ | 재기
원문 http://cafe.naver.com/ecpp/148



boost::shared_ptr
의 특징 중 하나는 boost::shared_ptr형 변수 선언 시 불완전한 클래스(incomplete class) 또는void형을 템플릿 인자로 사용할 수 있다는 사실입니다.


여기서 불완전한 클래스란 클래스의 선언부가 명백하게 컴파일 유닛에 포함(#include) 되지 않고 단순히 forward reference (or forward declaration) 로 선언 되어진 클래스를 말합니다.


컴파일 시간을 줄이기 위해서는 클래스가 선언된 헤더를 포함하지 않고 가능한 forward reference 를 이용하여 선언하는 방법(Compilation firewall, etc.)은 GOTW를 비롯한 여러 문헌에서 이미 자세하게 설명되어 있습니다.


boost::shared_ptr을 선언 시에 불완전한 클래스 또는void형을 템플릿으로 받을 수 있다는 특징을 보다 쉽게 이해하기 위해서 이러한 특징을 가지지 못하는std::auto_ptr과 비교해 봅니다.


#include<memory> // std::auto_ptr

#include<boost/shared_ptr.hpp>// boost::shared_ptr


// Forward reference.

classincomplete_type;


classMyClass1

{

 incomplete_type*p1_;

 void*p2_;

};


classMyClass2

{

 std::auto_ptr<incomplete_type>sp1_; // (1)

 std::auto_ptr<void>sp2_; // (2)

};


classMyClass3

{

 boost::shared_ptr<incomplete_type>sp1_; // (3)

 boost::shared_ptr<void>sp2_; // (4)

};


위의 예제를 컴파일하면 (1) 에서 컴파일 경고가 (2) 에서는 컴파일 오류가 발생합니다.


(1) 번 컴파일 경고는std::auto_ptr  형 변수가 삭제될 때 저장되어 있는 불완전한 클래스형 객체의 소멸자가 호출되지 않는다는 내용입니다.


c:\...\memory(718) :warningC4150:deletionofpointertoincompletetype'incomplete_type';nodestructorcalled

       x:\...\memory(717) :whilecompilingclasstemplatememberfunction'std::auto_ptr<_Ty>::~auto_ptr(void)'


이는std::auto_ptr형 변수가 선언 되어질 때에std::auto_ptr의 기본 생성자와 소멸자가 가시화 되는데 이 때std::auto_ptr의 소멸자에서 불완전한incomplete_type형 객체를 삭제하는delete문이 있기 때문입니다.


incomplete_type*pt= 0;

deletept;


이 문제는 위 코드를 컴파일 할 때 경고가 발생하는 것과 동일한 이치입니다. 특정 컴파일러에서는 이러한 경고 조차 발생하지 않는 경우도 있다고 합니다. 흔히std::auto_ptr을 pimpl 이디엄에 적용할 때 쉽게 이와 같은 문제를 접할 수 있습니다. 컴파일 오류를 발생시키는 대신 단지 경고만 주기 때문에 심각한 문제를 초래할 수도 있다는 것은 큰 문제점 이지만 컴파일 경고를 발생시키는 상황이 발생한다는 것 자체 부터std::auto_ptr의 단점이라고 할 수 있습니다.


(2) 번 오류의 경우는std::auto_ptr의 역참조 연산자 (operator *())가 템플릿 인자의 참조 형 즉void &를 리턴하려 하기 때문에 발생합니다. 이 컴파일 오류는 해결이 불가능합니다. 즉std::auto_ptr  은void형을 템플릿 인자로 받을 수 없습니다.


일반적으로 스마트 포인터는 raw 포인터의 특징을 최대한 유지한다는 디자인 패러다임 관점에서 보면std::auto_ptr의 위와 같은 제약들은 큰 결함이라고 할 수 있습니다. (반면에boost::shared_ptr은 불완전한 클래스나void형을 템플릿 인자로 받을 수 있습니다. 즉 raw 포인터의 특징을 그대로 계승하고 있다는 것을 알 수 있습니다.)


raw 포인터의 특징을 템플릿 클래스에서 어떻게 구현할 수 있을지 알아 봅니다.


template<classT>

classsimple_ptr

{

 T*pt_;


public:

 typedefTelement_type;


public:

 simple_ptr(T*pt= 0) :pt_(pt) { }

  ~simple_ptr() {if(pt_)deletept_; }


public:

 T*operator->()const{returnpt_; }

 Toperator*()const{return*pt_; }

};


가장 기초적인 형태의 위와 같은 스마트 포인터 클래스는 위에서 설명한std::auto_ptr이 가지는 문제점을 똑같이 내포하고 있습니다.


우선void  형을 템플릿 인자로 받을 수 있도록 하는 문제를 살펴 봅니다.


classMyClass

{

 simple_ptr<void>sp_;

};


위의 코드를 직접 컴파일 해보면void & simple_ptr<void>::operator *() const에서 컴파일 오류가 발생 한다는 사실을 알 수 있습니다. 이 문제는 언듯 쉽게 해결될 수 있을 것 처럼 보입니다.  다음과 같이 템플릿 특화를 이용한 trait 템플릿 클래스를 작성합니다.


template<classT>structsimple_ptr_traits

{

 typedefTreference;

};


template<>structsimple_ptr_traits<void>

{

 typedefvoidreference;

};


그리고 위의 trait 템플릿 클래스를 이용해simple_ptr를 아래와 같이 수정합니다.


template<classT>

classsimple_ptr

{

 T*pt_;


public:

 typedefTelement_type;

 typedeftypenamesimple_ptr_traits<T>::referencereference;


public:

 simple_ptr(T*pt= 0) :pt_(pt) { }

  ~simple_ptr() {if(pt_)deletept_; }


public:

 T*operator->()const{returnpt_; }

 referenceoperator*()const{return*pt_; }

};


이제 아래의 코드를 컴파일 해보면 더 이상 오류가 발생하지 않음을 알 수 있습니다.


classMyClass

{

 simple_ptr<void>sp_;

};


classFoo{ };


MyClass::MyClass() :sp_(newFoo) { }


더 이상 컴파일 오류는 발생하지 않지만 위의 코드는 여전히 문제점을 내재하고 있는 코드입니다.


simple_ptr<void>::pt_void형 포인터입니다. 따라서simple_ptr<void>의 생성자에서는Foo형 대상 객체의 포인터가 입력으로 주어졌을 때 void 형 포인터로 암시적 형 변환이 이루어졌습니다. 임의의 포인터는 항상 void 형 포인터로 암시적 형 변환이 가능합니다.

그런데simple_ptr<void>의 소멸자에서는void형 포인터가 삭제됩니다. C++에서void  형 포인터를 삭제하는 것은 합법이지만 삭제되는 포인터가 사용자 정의 클래스의 객체를 가리키고 있는 경우에 이 사용자 정의 클래스의 소멸자가 호출되지 않습니다. 따라서 사용자 정의 클래스의 소멸자에서 특별한 해제 작업을 하는 경우라면 큰 결함을 초래할 수 있습니다.


직접적으로void  형 문제는 아니지만 이와 비슷한 유형의 문제점이 하나 더 있습니다.


classBase

{

public:

 Base() { }

 ~Base() { }// non-virtual

};


classDerived:publicBase

{

 Derived() { }

 ~Derived() { }// non-virtual

};


simple_ptr<Base>sp(newDerived);


Dervied클래스 형 포인터는Base클래스 형 포인터로 암시적인 변환이 가능하기 때문에 위의 코드는 컴파일 오류는 발생하지 않습니다. 문제점은 simple_ptr<Base>::pt_Base클래스 형 포인터이고Base클래스의 소멸자가virtual로 선언되지 않았기 때문에simple_ptr<Base>형 변수sp가 삭제될 때Derived  클래스의 소멸자가 호출되지 않습니다.std::auto_ptr도 똑 같은 문제점을 가지고 있습니다.


이제 모든 문제는 "옳바른 소멸자가 호출되지 않는다" 라는 한가지 문제로 귀착되었습니다.


simple_ptr의 템플릿 인자로 주어지는 클래스 형에 관계 없이 실제simple_ptr로 할당 되어지는 객체 포인터의 클래스 형을 기억하고 있다가 소멸자에서 이 이 클래스 형으로 형 변환 후 객체를 삭제할 수 있기만 한다면 모든 문제가 해결 될 수 있습니다.


소멸자에서 옳바른 형 변환 후 삭제할 수 있게 하기 위해서 typeless deleter 를 작성 합니다. 여기서 편의 상 typeless deleter 는 타입 중성적인 (type neutral) 형태로 저장되지만 내부적으로 타입 안전(type safe) 하게 객체 포인터를 삭제하는 deleter 라고 정의 합니다.


template<classT>

classsimple_ptr

{

 T*pt_;


 typedefvoid(*typeless_deleter)(void*);

 typeless_deleterdeleter_;


 template<classU>

 structtyped_

  {

   staticvoiddeleter(void*typeless_obj)

    {

     U*typed_obj=static_cast<U*>(typeless_obj);

     deletetyped_obj;

    }

  };


public:

 typedefTelement_type;

 typedeftypenamesimple_ptr_traits<T>::referencereference;


public:

 simple_ptr()

    :pt_(0),deleter_(0)

  {

  }


 template<classU>

 simple_ptr(U*pu)

    :pt_(pu),deleter_(&typed_<U>::deleter)

  {

  }


  ~simple_ptr()

  {

   if(pt_&&deleter_)

      (*deleter_)(pt_);

  }


public:

 T*operator->()const{returnpt_; }

 referenceoperator*()const{return*pt_; }

};


위와 같은 구현은 여러가지 typeless deleter 구형 방법 중에서도 가장 간단한 형태라고 할 수 있습니다.simple_ptr의 내장 템플릿 클래스에 정의되어지는static멤버 함수를 통해서void형의 포인터를 옳바른 형 변환 후에 삭제 할 수 있습니다.static함수이기 때문에 특정 클래스에 한정되지 않고 타입 중성적인(type neutral) 형태로 deleter를 저장하는 것이 가능합니다.


simple_ptr의 소멸자는 더 이상 명시적으로delete를 사용하여 객체 포인터를 삭제하지 않습니다. 따라서 (1) 번과 같은 컴파일 경고가 발생하지 않을 뿐만 아니라 typeless deleter 를 통해서 객체 포인터가 옳바르게 삭제 된다는 것을 보장합니다.


simple_ptr의 두 번째 생성자에 주어지는 객체 포인터의 클래스 형을 위한 typeless deleter를 생성하기 때문에 상속 관계의 클래스에서 소멸자가virtual로 선언 되지 않은 경우라도 옳바른 소멸자가 호출되어진다는 것을 보장합니다.


simple_ptr<void>sp1(newFoo);

simple_ptr<Base>sp2(newDerived);


위의 코드에서 sp1의  typeless deleter는 simple_ptr<void>::typed_<Foo>::deleter이며sp2의 typeless deleter 는simple_ptr<Base>::typed_<Derived>::deleter가 됩니다. 따라서sp1이 스코프를 벗어나며 삭제 될 때에는 저장된void형 포인터가Foo형으로 형 변환된 후에 삭제되고sp2가 삭제될 때에는 저장된Base형 포인터가Derived형으로 형 변환된 후에 삭제됩니다.


structsimple_ptr<void>::typed_<Foo>

{

 staticvoiddeleter(void*typless_obj)

  {

   Foo*typed_obj=static_cast<Foo*>(typless_obj);

   deletetyped_obj;

  }

};


structsimple_ptr<Base>::typed_<Derived>

{

 staticvoiddeleter(void*typless_obj)

  {

   Derived*typed_obj=static_cast<Derived*>(typless_obj);

   deletetyped_obj;

  }

};


boost::shared_ptr의 typeless deleter는 사용자가 직접 제공할 수 있는 functor 를 사용한 형태의 typeless deleter 입니다. 즉 위 에제의 typeless deleter 에서delete typed_obj;위치에 사용자 정의 functor가 대신할 수 있다고 생각할 수 있습니다. 사용자가 직접 deleter를 제공할 수 있다는 사실이boost::shared_ptr을 더욱 강력하게 만들어 주지만 불완전한 클래스나void형을 템플릿 인자로 사용할 수 있는 기본 구현 원리는 위에서 구현한simple_ptr과  매우 유사하다고 볼 수 있습니다.


마지막으로 실용적이지는 못하지만 연습(!) 삼아서 boost::scoped_ptr이 불완전한 클래스나 void 형을 템플릿 인자로 받을 수 있도록 수정 해봅니다.


template<classT>structscoped_ptr_traits

{

 typedefTreference;

};


template<>structscoped_ptr_traits<void>

{

 typedefvoidreference;

};


template<classT>classscoped_ptr// noncopyable

{

private:


 T*ptr;


 scoped_ptr(scoped_ptrconst&);

 scoped_ptroperator=(scoped_ptrconst&);


 typedefscoped_ptr<T>this_type;

 typedeftypenamescoped_ptr_traits<T>::referencereference;


 typedefvoid(*typeless_deleter)(void*);

 typeless_deleterdeleter_;


 template<classU>

 structtyped_

  {

   staticvoiddeleter(void*typeless_obj)

    {

     U*typed_obj=static_cast<U*>(typeless_obj);

     boost::checked_delete(typed_obj);

    }

  };


public:


 typedefTelement_type;


 scoped_ptr() :ptr(0),deleter_(0) // never throws

  {

  }


 template<classU>

 explicitscoped_ptr(U*p):ptr(p),deleter_(&typed_<U>::deleter)// never throws

  {

  }


  ~scoped_ptr()// never throws

  {

   if(ptr&&deleter_)

    {

      (*deleter_)(ptr);

    }

  }


 voidreset()// never throws

  {

   this_type().swap(*this);

  }


 template<classU>

 voidreset(U*p)// never throws

  {

   this_type(p).swap(*this);

  }


 referenceoperator*()const// never throws

  {

   return*ptr;

  }


 T*operator->()const// never throws

  {

   returnptr;

  }


 T*get()const// never throws

  {

   returnptr;

  }


 typedefT*this_type::*unspecified_bool_type;


 operatorunspecified_bool_type()const// never throws

  {

   returnptr== 0? 0: &this_type::ptr;

  }


 booloperator! ()const// never throws

  {

   returnptr== 0;

  }


 voidswap(scoped_ptrb)// never throws

  {

   T*tmp=b.ptr;

   b.ptr=ptr;

   ptr=tmp;


   typeless_deletertmp_deleter=b.deleter_;

   b.deleter_=deleter_;

   deleter_=tmp_deleter;

  }

};


'Programming > MFC' 카테고리의 다른 글

Visual Studio .Net 단축키 모음  (0) 2008.02.26
BSTR CString conversions  (0) 2008.01.17
trl::shared_ptr 사용방법  (0) 2008.01.10
MessgeBox 졸료하기  (0) 2008.01.08
BROWSEINFO 의 초기경로 설정  (0) 2007.11.09