본문 바로가기

프로그래밍

[스크랩]tr1::shared_ptr

출처:sweeper.egloos.com/2826435



1. auto_ptr

TR1이 발표되기 전까지 std::auto_ptr이 C++ Standara library의 유일한 스마트 포인터였다.

스마트 포인터의 기본적인 특성인 자신이 소멸될 때 가리키고 있는 대상에 대해 자동으로 delete 해줘 메모리 누수 걱정은 없게 작성이 되어 있다.

하지만, auto_ptr은 유일 소유권 개념이 있어서, 객체가 복사되는 순간(복사생성 또는 대입연산) 원래의 auto_ptr은 바로 NULL 처리가 되어 버린다.

  1. class AAA;
  2.  
  3. // RAII 방식으로... AAA 객체 생성
  4. std::auto_ptr<AAA> AAAObject(new AAA());
  5.  
  6. // 복사가 되는 순간, AAAObject는 NULL이 되고, 이제 BBBObject 만이 객체를 가리킨다.
  7. std::auto_ptr<AAA> BBBObject(AAAObject);
  8.  
  9. // 역시 대입이 되는 순간, BBB는 NULL, 이제 AAA가 객체를 가리킴.
  10. AAAObject = BBBObject;

이렇듯 괴상망측한 복사 동작으로 인해 STL의 컨테이너에서도 전혀 환영받지 못하고, (STL 컨테이너들은 정상적인 복사 능력을 가진 원소를 요구한다) 일반적인 프로그래머들 사이에서도 몹쓸 녀석이 되어 버렸다.

나 같은 경우도 의미 파악을 위해 MSDN 보고 클래스 한 두번 만들어본 게 전부이지, 실무에 써먹은 적은 한번도 없다.
앞으로도 영원히 쓸 일이 없지 싶으다 -_-;


2. boost::shared_ptr의 등장

저렇듯 C++ Standard Library에 유일하게 하나 있는 스마트 포인터가 병X이다 보니 부스트 형님들이 가만 있지 않았고,
곧 바로 참조 카운팅 방식을 사용하는 스마트 포인터, boost::shared_ptr을 내놓게 된다.

즉, shared_ptr은 특정 자원을 가리키는 참조 카운트를 유지하고 있다가 이것이 0 이 되면 해당 자원을 자동으로 삭제해 주는 스마트 포인터인 것이다.

참조 카운트는 이를 가리키는 외부 객체의 수가 증가할 때 같이 올라간다.
즉, shared_ptr의 복사나 대입이 발생하면 레퍼런스 카운트가 증가하고, 그 복사/대입되었던 녀석들이 소멸되게 되면 레퍼런스 카운트가 감소하는 것이다.

우선, 이 문서는 boost::shared_ptr의 소개를 위한 문서가 아니므로, 이처럼 개념적인 부분만 정리하고 나머지는 링크로 대체한다.

boost official homepage : www.boost.org (참고로 2011/07/11 부스트 라이브러리는 1.47.0으로 업데이트 되었다)


3. TR1::shared_ptr

C++ Standard Library는 많은 것을 포함하고 있는, 엄청나게 방대한 라이브러리이지만 세월이 흐름에 따라 새로운 요구 사항들이 계속해서 발생하게 되었다.
특히 이러한 패러다임의 충실하게 반영되고 있는 boost 진영의 소리없는 압박도 C++ Standard Library의 변화 유발을 촉구시켰다.

2005년 5월, 드디어 Technical Report 1, 즉 TR1이 발표되었고, 여기엔 꽤나 많은 부분들이 추가되었다.

이들의 대부분은 boost 진영에서 개발되어 전세계 수많은 프로그래머들에 의해 이미 검증된 것들이고, 그 중 하나가 TR1::shared_ptr (from boost::shared_ptr) 이다.

TR1::shared_ptr (이하 shared_ptr이라고만 쓰겠음)은 태생이 boost::shared_ptr이라 거의 모든 구현 내용이 boost::shared_ptr과 똑같다.

우선 MSDN 링크는 다음과 같다 : http://msdn.microsoft.com/ko-kr/library/bb982026

아래 사용법들과 예제를 통해 shared_ptr의 특징을 간단히 정리해 보자.

1) namespace와 필요 헤더 파일
  • namespace : std
  • header : <memory>

2) 선언

shared_ptr의 선언은 아래와 같이 RAII idiom을 따른다.

  1. class Car {...};
  2.  
  3. // Resource Acquisition Is Initializing : RAII
  4. std::shared_ptr<Car> Avante( new Car() );

즉, std::shared_ptr<_Ty> Object( new _Ty(construct) );의 형식을 띈다.


3) Reference count의 증가와 감소
  • 증가 : shared_ptr 객체의 복사나 대입이 발생하여 참조 shared_ptr 객체 수 증가.
  • 감소 : shared_ptr이 가리키고 있는 객체를 참조하는 shared_ptr 객체 수의 감소.

<참조 카운트 예제>

  1. class Car {...};
  2.  
  3. // 값 전달, 복사에 의한 임시객체 생성, 함수 종료시 생성된 임시객체 소멸
  4. // 하지만 아래 매개변수를 const std::shared_ptr<Car>&로 받는다면,
  5. // 임시 객체가 생기지 않아서 참조 카운트가 올라가지 않는다.
  6. void function( std::shared_ptr<Car> _car )
  7. {
  8.         ...
  9. }
  10.  
  11. int main()
  12. {
  13.         // 최초 생성시 초기 참조 카운트는 당연히 '1'
  14.         std::shared_ptr<Car> Car1( new Car() );
  15.         // 복사 -> 참조 카운트 '2'
  16.         std::shared_ptr<Car> Car2(Car1);
  17.         // 대입 -> 참조 카운트 '3'
  18.         std::shared_ptr<Car> Car3 = Car1;
  19.  
  20.         // function( std::shared_ptr<Car> _car ), 값에 의한 전달, 복사에 의한 임시객체 생성
  21.         // 이로 인한 참조 카운트 증가 -> '4'
  22.         function( Car3 );
  23.         // 함수 호출 후엔 임시객체 소멸되므로 참조 카운트 감소 -> '3'
  24.  
  25.         // reset 함수는 shared_ptr이 참조하는 객체를 새로운 녀석으로 바꿀 수 있는 함수이다.
  26.         // 내부적으로 shared_ptr::swap 함수가 사용됨
  27.         // http://msdn.microsoft.com/ko-kr/library/bb982757.aspx
  28.         // 인자를 주지 않으면 참조 포기가 되는 것이다. 따라서 참조 카운트 감소 -> '2'
  29.         Car3.reset();
  30.         ...
  31.         return 0;
  32.         // 함수 반환시 남아있던 shared_ptr 모두 소멸 -> 참조 카운트 '0'
  33.         // 이제 shared_ptr이 참조하고 있던 Car * 에 대해 delete가 호출됨.
  34. }


4) shared_ptr의 참조 해제

shared_ptr의 refCount == 1 인 상태에서 원래 참조하고 있던 객체가 아닌 다른 객체를 참조하게 되면, 원래 참조하고 있던 객체는 delete 처리가 된다.

이해엔 예제가 따봉~

  1. class Car {...};
  2.  
  3. // 최초 생성시 초기 참조 카운트는 당연히 '1'
  4. std::shared_ptr<Car> Car1( new Car() );
  5. // 최초 생성시 초기 참조 카운트는 당연히 '1'
  6. std::shared_ptr<Car> Car2( new Car() );
  7.  
  8. // Car1 shared_ptr은 이제 Car2의 객체를 참조한다.
  9. // Car1이 참조하던 Car* 는 더 이상 참조자가 존재하지 않아, delete가 호출된다.
  10. // 대신 Car2가 참조하던 객체를 이제 Car1 shared_ptr도 참조하므로 참조 카운트는 '2'
  11. Car1 = Car2;


5) shared_ptr 소멸시 주의사항

기본적으로, shared_ptr은 소멸시 참조 카운트가 0 이 되면, 참조하는 객체에 대해 delete 연산자를 사용한다.
응?! delete만 사용한다는 소리다. 즉, delete [] 따윈 사용해 주지 않는단 말이다.

따라서, 아래와 같이 하면 new-delete, new [] - delete []를 지키지 않았을 때의 문제가 그대로 나타나는 것이다.

std::shared_ptr<int> spi( new int[1024] );

이는 vector등으로 표현할 수 있기에 굳이 TR1에 포함되지 않았을 것이라고 추측해 보지만, 뭐 불편하긴 하다.
즉, 아래와 같이 하라는 것이다.

std::vectorstd::shared_ptr<int> > spVec;
spVec.push_backstd::shared_ptr<int>( new int(3) ) );

(부스트의 scoped_array나 shared_array가 그리운가? 쩝;;;)

위 방법 외에도 배열 삭제를 지원하는 deleter를 지정하여 해결할 수도 있다.

이는 다음 "deleter 지정"에서 설명하겠다.


6) deleter 지정

shared_ptr의 생성자 함수는 크게 다음 세 가지 형태로 정의되어 있다.

  1. template<class _Ux>
  2. explicit shared_ptr(_Ux *_Px)
  3. {       // construct shared_ptr object that owns _Px
  4.         _Resetp(_Px);
  5. }
  6.  
  7. template<class _Ux, class _Dx>
  8. shared_ptr(_Ux *_Px, _Dx _Dt)
  9. {       // construct with _Px, deleter
  10.         _Resetp(_Px, _Dt);
  11. }
  12.  
  13. template<class _Ux, class _Dx, class _Alloc>
  14. shared_ptr(_Ux *_Px, _Dx _Dt, _Alloc _Ax)
  15. {       // construct with _Px, deleter, allocator
  16.         _Resetp(_Px, _Dt, _Ax);
  17. }

두 번째 생성자의 정의부터 보이는 class _Dx를 우리가 정의한 클래스로 지정시, 
이는 shared_ptr의 참조 카운트가 0 이 될 때의 deleter 클래스가 된다.

예제 1) 배열 타입의 deleter

  1. // deleter 클래스 정의
  2. template<typename T>
  3. struct ArrayDeleter
  4. {      
  5.         void operator () (T* p)
  6.         {
  7.                 delete [] p;
  8.         }
  9. };
  10.  
  11. // shared_ptr 생성시 두 번째 인자로 deleter class를 넘기면...
  12. // 아무런 문제없이 객체 배열도 제대로 delete [] 처리가 된다.
  13. std::shared_ptr<int> spi( new int[1024], ArrayDeleter<int>() );

예제 2) Empty deleter


7) 참조 객체 형변환

shared_ptr 비멤버 함수를 통해 shared_ptr이 참조하고 있는 객체의 형 변환을 수행할 수 있다.

(참고로, shared_ptr의 모든 operator 연산자 역시 이처럼 비멤버 함수로 구현되어 있다)

  1. template<class _Ty1, class _Ty2>
  2. shared_ptr<_Ty1> static_pointer_cast(const shared_ptr<_Ty2>& _Other)
  3. {      
  4.         // return shared_ptr object holding static_cast<_Ty1 *>(_Other.get())
  5.         return (shared_ptr<_Ty1>(_Other, _Static_tag()));
  6. }
  7.  
  8. template<class _Ty1, class _Ty2>
  9. shared_ptr<_Ty1> const_pointer_cast(const shared_ptr<_Ty2>& _Other)
  10. {      
  11.         // return shared_ptr object holding const_cast<_Ty1 *>(_Other.get())
  12.         return (shared_ptr<_Ty1>(_Other, _Const_tag()));
  13. }
  14.  
  15. template<class _Ty1, class _Ty2>
  16. shared_ptr<_Ty1> dynamic_pointer_cast(const shared_ptr<_Ty2>& _Other)
  17. {      
  18.         // return shared_ptr object holding dynamic_cast<_Ty1 *>(_Other.get())
  19.         return (shared_ptr<_Ty1>(_Other, _Dynamic_tag()));
  20. }

예제)

  1. class Car {...};
  2. class Truck : public Car {...};
  3.  
  4. // Truck 타입의 객체를 Car 타입의 객체를 참조하는 shared_ptr에 초기화
  5. shared_ptr<Car> pCar( new Truck() );
  6.  
  7. // shared_ptr<Car>가 참조하고 있던 객체를 Truck 타입으로 static_cast하여 대입.
  8. // 대입 하였기에 참조 카운트는 '2'
  9. shared_ptr<Truck> pTruck = static_pointer_cast<Truck>(pCar);
  10.  
  11. // 위처럼 대입하지 않고 스스로 형변환만 하여도 상관없음.
  12. // 참조 카운트는 당연히 변화가 없다.
  13. static_pointer_cast<Car>(pCar);


8) 참조 객체 접근

shared_ptr이 참조하는 실제 객체를 얻는 방법은 명시적/암시적의 두 가지 방법이 있다.

명시적 방법
  • shared_ptr::get()
    : 참조하고 있는 객체의 주소를 반환한다.

암시적 방법
  • shared_ptr::operator*
    : 참조하고 있는 객체 자체를 반환한다.
    : 즉, *(get())의 의미

  • shared_ptr::operator->
    : get()->의 의미가 같다.

예제)

  1. shared_ptr<Car> spCar( new Truck() );
  2.  
  3. // spCar가 참조하는 객체의 주소를 반환
  4. Car* pCar = spCar.get();
  5.  
  6. // spCar가 참조하는 객체의 메써드에 접근 #1
  7. spCar.get()->MemberFunc();
  8.  
  9. // spCar가 참조하는 객체의 메써드에 접근 #2
  10. *(spCar).MemberFunc();
  11.  
  12. // spCar가 참조하는 객체의 메써드에 접근 #3
  13. spCar->MemberFunc();


9) 멀티 쓰레드 안정성

MSDN에 보면 shared_ptr의 멀티 쓰레드 안정성에 대해 다음과 같이 얘기하고 있다.

Multiple threads can simultaneously read and write different shared_ptr objects, even when the objects are copies that share ownership.

하지만, shard_ptr의 내부 소스를 아무리 뒤져봐도 reference count에 대한 동기화는 보장이 되나, 참조하고 있는 객체에 대한 동기화 보장에 대한 내용은 없다.

이에 마침 검색을 해보니 다음 블로그 링크를 찾게 되었다.

http://process3.blog.me/20049917212


즉, 결론만 이야기하면, 레퍼런스 카운트에 대해서만 동기화를 해서 멀티 쓰레드에서의 안정성을 얻는다.


10) 적절한 활용 예시

포인터를 담는 벡터에 대한 내용인데, 괜찮아서 링크 건다.

http://sanaigon.tistory.com/72