티스토리 뷰

STL

[BOOST / TR1] std::tr1::shared_ptr

hoiogi 2010. 9. 3. 10:23

1. delete를 직접 하지말고 자원관리객체에게 맡겨라

T
 *p = new T;
위의 코드를 다음으로 바꿉니다.
std::tr1::shared_ptr<T> p( new T );

자원을 획득하자마자 자원관리 객체의 초기화 코드로 넘기는데 이를 자원획득즉초기화(RAII) 라 합니다.


2. delete[] 와 shared_ptr

 shared_ptr 의 생성자는 타입T 의 포인터, 즉 T* 만을 받는 생성자가 있고 T* 와 삭제자(deleter) 를 받는 생성자도 있습니다.
 삭제자를 넘겨주지 않는 첫 번째의 생성자같은 경우 shared_ptr 는 자신이 소멸할 때 자원에 delete 를 적용해줍니다. 근데 만약 아래처럼

std::tr1::shared_ptr<T> p( new T[10] );

동적 배열 자원을 넘겨주게 되면 new[] 로 할당된 자원을 delete[] 가 아닌 delete 로 반납을 행하는데 이런 경우오작동 또는 메모리 누수가 발생하게 됩니다. 이를 막기위해 두 가지 방식을 취할 수 있습니다.

첫 번째는 vector 와 같은 컨테이너로 여러 shared_ptr 객체를 이용하는 방법입니다.

std::vector< std::tr1::shared_ptr<T> > v( 10 );
v[0] = std::tr1::shared_ptr<T>( new T );
...

두 번째는 delete[] 를 이용하는 삭제자를 정의( 일종의 함수객체 ) 하고 shared_ptr 의 생성자로 넘겨주는 방법입니다.

struct arrayDeleter
{
  template< typename T >
  void operator()( T *p )
  {
     delete[] p;
  }
};

std::tr1::shared_ptr<T> p( new T[10], arrayDeleter() );

이렇게 하면 정상적으로 자원의 할당 / 반납이 행해집니다.

세 번째는 shared_array 사용


3. 삭제자의 다양한 활용

int a = 0;
std::tr1::shared_ptr<int> p( &a );

어떤 변수를 포인터로 가리키는 경우는 자주 볼 수 있습니다. 그와 비슷하게 shared_ptr 로 지역변수 a 를 가리키도록 한 것이 위 의 코드입니다. 그러나, 객체 p가 소멸할 때 &a 에 delete 를 적용하기 때문에 결과적으로 delete &a; 동작이 소멸 과정에서 행해집니다. 여기서 a 는 동적 자원이 아닌 지역변수이기 때문에 에러가 발생하게 됩니다. 이를 막기위해 소멸과정에서 어떠한 동작도 행하지 않도록 빈 삭제자( Empty deleter ) 를 지정할 수 있습니다.

struct emptyDeleter
{
  template< typename T >
  void operator()( T *p ){}
};

int a = 0;
std::tr1::shared_ptr<int> p( &aemptyDeleter() );

이 번에는 C버전 파일입출력 라이브러리에서 fclose() 함수 호출을 shared_ptr 를 이용하여 자동으로 처리되도록 하는 예입니다.

struct fileCloser
{
  void operator( FILE *f )
  {
    if( f ){ fclose( f ); }
  }
};

std::tr1::shared_ptr<FILE> f( fopen( "test.txt", "rb" ), fileCloser() );


4. shared_ptr<T> <-> T*

가끔 shared_ptr 객체가 관리하는 자원을 얻어내야 하는 경우가 있는데 그 땐 shared_ptr<T>::get() 멤법함수를 이용합니다.

std::tr1::shared_ptr<T> p( new T );
T *ptr = p.get();  // p.get() 은 T* 를 반환합니다.


5. 자원의 교체

shared_ptr 객체가 관리하고 있는 자원을 다른 자원 또는 NULL 로 만들어야 하는 경우는 shared_ptr<T>::reset() 멤버함수를 이용합니다.

std::tr1::shared_ptr<T> p( new T );
p.reset();        // 이제 p는 자원을 관리하지 않습니다. 기존의 자원은 안전하게 delete 됩니다.
p.reset( new T );     // p는 새로운 자원을 관리합니다.
p = std::tr1::shared_ptr<T>();   // 빈 shared_ptr 객체를 대입합니다. p.reset(); 과 동일합니다.


6. 복사와 참조 카운팅

shared_ptr 객체는 복사될 때마다 자원을 복사하는게 아닌 자원의 참조개수를 증가시킵니다.

std::tr1::shared_ptr<int> p( new int );   // 자원은 p에 의해 1번 참조됩니다.
std::tr1::shared_ptr<int> p2( p );    // 자원은 p, p2에 의해 2번 참조됩니다.
p.reset();                 // 자원은 p2에 의해 1번 참조됩니다.
p = p2;                // 자원은 p, p2에 의해 2번 참조됩니다.
p2.reset();               // 자원은 p에 의해 1번 참조됩니다.
p.reset();                 // 자원은 더 이상 참조되지 않음으로 delete 됩니다.


7. 조건문과 shared_ptr

기존 포인터를 이용할 때 유효한 포인터인지 확인하기 위해

int *p = &a;
...
if( p != NULL )
{ ... }

위의 코드처럼 if 를 이용하여 NULL 과 비교하곤 했습니다. 이를 shared_ptr 에서는 다음처럼 작성합니다.

std::tr1::shared_ptr<int> p( new int );
...
if( !p )
{ ... }

위에서 !p 처럼 이용하면 됩니다. 즉 shared_ptr 객체는 bool 식으로 변환될 수 있습니다.


8. const 자원과 shared_ptr

상수 자원을 관리하거나 자원을 상수화시키기 위해 const 를 이용할 수 있습니다.

std::tr1::shared_ptr<int> p( new int );    // p객체는 언제든지 자원을 변경할 수 있습니다.
std::tr1::shared_ptr<int const> q( new int ); // q객체는 자원을 읽을수는 있으나 변경할 수는 없습니다.

p = q;                   // 자원을 변경하지 못하는 q객체를 자원이 변경가능한 p에 복사합니다.
                     // 그러나 이 코드는 컴파일되지 않습니다.

q = p;                   // 자원이 변경가능한 p객체를 자원을 변경하지 못하는 q객체에 복사합니다.
                     // 컴파일이 잘 됩니다.

기존 포인터에서 적용되던 상수성을 그대로 유지해줍니다.


9. 상속과 shared_ptr

Child 클래스가 Parent 클래스를 public 상속했다고 할 때 기존 포인터를 이용하여

Parent *p = new Child;

위와 같이 작성할 수 있습니다. 이를 shared_ptr 로 작성하면 다음과 같습니다.

std::tr1::shared_ptr<Parent> p( new Child );

다형성을 이용하는 곳에도 별다른 지장없이 shared_ptr 를 이용할 수 있습니다.


10. typedef 활용

shared_ptr 를 이용하면 타입이름이 너무 길어지게 되는데 여기에 typedef 를 활용합니다.

class CMyClass;
typedef std::tr1::shared_ptr<CMyClassCMyClass_ptr;

class CMyClass
{
...
  CMyClass_ptr getThis(){ return CMyClass_ptr( this, emptyDeleter() ); }
...
};

위처럼 std::tr1::shared_ptr<T> 타입을 T_ptr 로 typedef 하는 방법입니다.

this 포인터를 넘기는 또다른 방법
boost::enable_shared_from_this 를 사용(http://naiades.tistory.com/tag/enable_shared_from_this)


11. 순환참조 문제

사람 클래스가 있고 멤버로 친구가 있다고 하겠습니다.

struct person
{
  std::string name;
  std::tr1::shared_ptr<person> myFriend;

  person( const std::string &_name ) : name( _name )
  {
    std::cout << name << "태어나다." << std::endl;
  }
  ~person()
  {
    std::cout << name << "죽다." << std::endl;
  }
};
그리고 아래와 같은 블럭이 실행되면 생성자는 제대로 호출되지만 소멸자가 호출되지 않는 문제가 발생합니다.

{
   std::tr1::shared_ptr<person> kim( new person( "kim" ) );
   std::tr1::shared_ptr<person> lee( new person( "lee" ) );

   kim->myFriend = lee;
   lee->myFriend = kim;
}

 원인을 알아보겠습니다. 블럭의 끝에서 lee 객체가 먼저 소멸하는데 참조개수가 둘( 자신과 kim의 친구로) 이므로 참조개수를 하나 줄이고 소멸은 끝납니다. 즉 lee 의 참조개수는 1이고 kim의 참조개수는 2입니다.
 다음 kim 객체가 소멸하는데 마찬가지로 참조개수가 둘( 자신과 lee의 친구로 ) 이므로 참조개수를 하나 줄이고 소멸이 끝납니다. 결과적으로.. kim, lee 둘 다 참조개수가 1인 상태로 남아있게 되는 문제가 발생했습니다.

 이렇게 다른 객체끼리 서로 참조를 갖는 형태를 순환참조라 하며 shared_ptr 는 순환참조시 제대로 자원해제를 하지 못합니다.
 이런 경우는 친구를 shared_ptr<person> 타입이 아닌 person* 타입으로 바꾸거나 빈 삭제자를 이용하거나 또 다른 스마트포인터중 하나인 weak_ptr 를 이용하여 해결할 수 있습니다. person* 타입으로 바꾸는 것은 쉽기 때문에 생략하고

먼저 빈 삭제자를 이용하는 방법을 말씀드리겠습니다.

아 래의 코드 대신

kim->myFriend = lee;
lee->myFriend = kim;

다음의 코드를 이용합니다.

lee->myFriend.reset( kim.get(), emptyDeleter() );
kim->myFriend.reset( lee.get(), emptyDeleter() );

이제 정상적으로 소멸되는 것을 확인할 수 있습니다.(두번 삭제 방지)

이 번엔 weak_ptr 을 이용해보겠습니다. 그 전에 weak_ptr 의 특징은 shared_ptr 객체를 받을 수 있으며 자원의 참조개수에 변화를 주지 않습니다. 그렇기 때문에 이런 순환문제에도 적절하게 동작하게 됩니다.

person 의 멤버변수를 아래의 코드 대신

std::tr1::shared_ptr<person> myFriend;

다음의 코드를 이용합니다.

std::tr1::weak_ptr<person> myFriend;

나머지 코드는 동일합니다. 마찬가지로 정상적으로 소멸되는 것을 확인할 수 있습니다.


출처 : http://yucherrypl.tistory.com/8848

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함