Boost 스마트 포인터

2014. 6. 11. 10:13C++

http://sweeper.egloos.com/2826435



[TR1] shared_ptr TR1 / C++11


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) Circular references

레퍼런스 카운팅 기반이기에 순환 참조에 대한 잠재적인 문제가 있을 수 있다.
즉, A와 B가 서로에 대한 shared_ptr을 들고 있으면 레퍼런스 카운트가 0이 되지 않아 메모리가 해제되지 않는다.

게임의 경우를 예로 들어, 파티 객체가 있으면 파티원 객체가 있을 것이다.
파티 객체는 파티원 객체를 리스트 형태로 들고 있고, 또 파티원도 자기가 속한 파티 객체를 알기 위해 참조 형태로 들고 있는 예제를 살펴보자.

  1. #include <memory>    // for shared_ptr
  2. #include <vector>
  3.  
  4. using namespace std;
  5.  
  6. class User;
  7. typedef shared_ptr<User> UserPtr;
  8.  
  9. class Party
  10. {
  11. public:
  12.     Party() {}
  13.     ~Party() { m_MemberList.clear(); }
  14.  
  15. public:
  16.     void AddMember(const UserPtr& member)
  17.     {
  18.         m_MemberList.push_back(member);
  19.     }
  20.  
  21. private:
  22.     typedef vector<UserPtr> MemberList;
  23.     MemberList m_MemberList;
  24. };
  25. typedef shared_ptr<Party> PartyPtr;
  26.  
  27. class User
  28. {
  29. public:
  30.     void SetParty(const PartyPtr& party)
  31.     {
  32.         m_Party = party;
  33.     }
  34.  
  35. private:
  36.     PartyPtr m_Party;
  37. };
  38.  
  39.  
  40. int _tmain(int argc, _TCHAR* argv[])
  41. {
  42.     PartyPtr party(new Party);
  43.  
  44.     for (int i = 0; i < 5; i++)
  45.     {
  46.         // 이 user는 이 스코프 안에서 소멸되지만,
  47.         // 아래 party->AddMember로 인해 이 스코프가 종료되어도 user의 refCount = 1
  48.         UserPtr user(new User);
  49.  
  50.         // 아래 과정에서 순환 참조가 발생한다.
  51.         party->AddMember(user);
  52.         user->SetParty(party);
  53.     }
  54.  
  55.     // 여기에서 party.reset을 수행해 보지만,
  56.     // 5명의 파티원이 party 객체를 물고 있어 아직 refCount = 5 의 상태
  57.     // 따라서, party가 소멸되지 못하고, party의 vector에 저장된 user 객체들 역시 소멸되지 못한다.
  58.     party.reset();
  59.  
  60.     return 0;
  61. }

위와 같은 형태로 shared_ptr이 서로를 참조하고 있는 것은 circular reference라고 한다.

위 예제처럼, 그룹 객체 - 소속 객체간 상호 참조는 실무에서도 흔히 볼 수 있는 패턴이며, 
보통은 위 예제처럼 직접 참조 형식이 아니라, User는 PartyID를 들고 있고, Party 객체에 접근 필요시 PartyManger(컬렉션)에 질의해서 유효한 Party 객체 포인터를 얻어오는 방식을 사용한다.

그렇다고, PartyManager에 일일히 ID로 검색하는 비용을 줄이고자, Party 포인터를 직접 들고 있으면, 들고 있던 포인터가 dangling pointer가 될 수 있는 위험이 있다.

이럴 때, User 객체가 Party 객체를 shared_ptr가 아닌 weak_ptr을 사용하여 들고 있다면, 검색 비용 회피와 dangling pointer의 위험에서 모두 벗어날 수 있다.

std::weak_ptr은 shared_ptr로부터 생성될 수 있고, shared_ptr이 가리키고 있는 객체를 똑같이 참조하지만, 참조만 할 뿐 reference counting은 하지 않아 위 예제의 목적에 가장 바람직한 대안이 될 수 있다 할 수 있다.


10) 멀티 쓰레드 안정성

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


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


11) 적절한 활용 예시

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

http://sanaigon.tistory.com/72