1. auto_ptr
TR1이 발표되기 전까지 std::auto_ptr이 C++ Standara library의 유일한 스마트 포인터였다.
스마트 포인터의 기본적인 특성인 자신이 소멸될 때 가리키고 있는 대상에 대해 자동으로 delete 해줘 메모리 누수 걱정은 없게 작성이 되어 있다.
하지만, auto_ptr은 유일 소유권 개념이 있어서, 객체가 복사되는 순간(복사생성 또는 대입연산) 원래의 auto_ptr은 바로 NULL 처리가 되어 버린다.
- class AAA;
- // RAII 방식으로... AAA 객체 생성
- std::auto_ptr<AAA> AAAObject(new AAA());
- // 복사가 되는 순간, AAAObject는 NULL이 되고, 이제 BBBObject 만이 객체를 가리킨다.
- std::auto_ptr<AAA> BBBObject(AAAObject);
- // 역시 대입이 되는 순간, BBB는 NULL, 이제 AAA가 객체를 가리킴.
- 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으로 업데이트 되었다)
boost smart pointer : http://www.boost.org/doc/libs/1_47_0/libs/smart_ptr/smart_ptr.htm
boost::share_ptr : http://www.boost.org/doc/libs/1_47_0/libs/smart_ptr/shared_ptr.htm
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을 따른다.
- class Car {...};
- // Resource Acquisition Is Initializing : RAII
- 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 객체 수의 감소.
<참조 카운트 예제>
- class Car {...};
- // 값 전달, 복사에 의한 임시객체 생성, 함수 종료시 생성된 임시객체 소멸
- // 하지만 아래 매개변수를 const std::shared_ptr<Car>&로 받는다면,
- // 임시 객체가 생기지 않아서 참조 카운트가 올라가지 않는다.
- void function( std::shared_ptr<Car> _car )
- {
- ...
- }
- int main()
- {
- // 최초 생성시 초기 참조 카운트는 당연히 '1'
- std::shared_ptr<Car> Car1( new Car() );
- // 복사 -> 참조 카운트 '2'
- std::shared_ptr<Car> Car2(Car1);
- // 대입 -> 참조 카운트 '3'
- std::shared_ptr<Car> Car3 = Car1;
- // function( std::shared_ptr<Car> _car ), 값에 의한 전달, 복사에 의한 임시객체 생성
- // 이로 인한 참조 카운트 증가 -> '4'
- function( Car3 );
- // 함수 호출 후엔 임시객체 소멸되므로 참조 카운트 감소 -> '3'
- // reset 함수는 shared_ptr이 참조하는 객체를 새로운 녀석으로 바꿀 수 있는 함수이다.
- // 내부적으로 shared_ptr::swap 함수가 사용됨
- // http://msdn.microsoft.com/ko-kr/library/bb982757.aspx
- // 인자를 주지 않으면 참조 포기가 되는 것이다. 따라서 참조 카운트 감소 -> '2'
- Car3.reset();
- ...
- return 0;
- // 함수 반환시 남아있던 shared_ptr 모두 소멸 -> 참조 카운트 '0'
- // 이제 shared_ptr이 참조하고 있던 Car * 에 대해 delete가 호출됨.
- }
4) shared_ptr의 참조 해제
shared_ptr의 refCount == 1 인 상태에서 원래 참조하고 있던 객체가 아닌 다른 객체를 참조하게 되면, 원래 참조하고 있던 객체는 delete 처리가 된다.
이해엔 예제가 따봉~
- class Car {...};
- // 최초 생성시 초기 참조 카운트는 당연히 '1'
- std::shared_ptr<Car> Car1( new Car() );
- // 최초 생성시 초기 참조 카운트는 당연히 '1'
- std::shared_ptr<Car> Car2( new Car() );
- // Car1 shared_ptr은 이제 Car2의 객체를 참조한다.
- // Car1이 참조하던 Car* 는 더 이상 참조자가 존재하지 않아, delete가 호출된다.
- // 대신 Car2가 참조하던 객체를 이제 Car1 shared_ptr도 참조하므로 참조 카운트는 '2'
- 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::vector< std::shared_ptr<int> > spVec;
spVec.push_back( std::shared_ptr<int>( new int(3) ) );
(부스트의 scoped_array나 shared_array가 그리운가? 쩝;;;)
위 방법 외에도 배열 삭제를 지원하는 deleter를 지정하여 해결할 수도 있다.
이는 다음 "deleter 지정"에서 설명하겠다.
6) deleter 지정
shared_ptr의 생성자 함수는 크게 다음 세 가지 형태로 정의되어 있다.
- template<class _Ux>
- explicit shared_ptr(_Ux *_Px)
- { // construct shared_ptr object that owns _Px
- _Resetp(_Px);
- }
- template<class _Ux, class _Dx>
- shared_ptr(_Ux *_Px, _Dx _Dt)
- { // construct with _Px, deleter
- _Resetp(_Px, _Dt);
- }
- template<class _Ux, class _Dx, class _Alloc>
- shared_ptr(_Ux *_Px, _Dx _Dt, _Alloc _Ax)
- { // construct with _Px, deleter, allocator
- _Resetp(_Px, _Dt, _Ax);
- }
두 번째 생성자의 정의부터 보이는 class _Dx를 우리가 정의한 클래스로 지정시,
이는 shared_ptr의 참조 카운트가 0 이 될 때의 deleter 클래스가 된다.
예제 1) 배열 타입의 deleter
- // deleter 클래스 정의
- template<typename T>
- struct ArrayDeleter
- {
- void operator () (T* p)
- {
- delete [] p;
- }
- };
- // shared_ptr 생성시 두 번째 인자로 deleter class를 넘기면...
- // 아무런 문제없이 객체 배열도 제대로 delete [] 처리가 된다.
- std::shared_ptr<int> spi( new int[1024], ArrayDeleter<int>() );
예제 2) Empty deleter
7) 참조 객체 형변환
shared_ptr 비멤버 함수를 통해 shared_ptr이 참조하고 있는 객체의 형 변환을 수행할 수 있다.
(참고로, shared_ptr의 모든 operator 연산자 역시 이처럼 비멤버 함수로 구현되어 있다)
- template<class _Ty1, class _Ty2>
- shared_ptr<_Ty1> static_pointer_cast(const shared_ptr<_Ty2>& _Other)
- {
- // return shared_ptr object holding static_cast<_Ty1 *>(_Other.get())
- return (shared_ptr<_Ty1>(_Other, _Static_tag()));
- }
- template<class _Ty1, class _Ty2>
- shared_ptr<_Ty1> const_pointer_cast(const shared_ptr<_Ty2>& _Other)
- {
- // return shared_ptr object holding const_cast<_Ty1 *>(_Other.get())
- return (shared_ptr<_Ty1>(_Other, _Const_tag()));
- }
- template<class _Ty1, class _Ty2>
- shared_ptr<_Ty1> dynamic_pointer_cast(const shared_ptr<_Ty2>& _Other)
- {
- // return shared_ptr object holding dynamic_cast<_Ty1 *>(_Other.get())
- return (shared_ptr<_Ty1>(_Other, _Dynamic_tag()));
- }
예제)
- class Car {...};
- class Truck : public Car {...};
- // Truck 타입의 객체를 Car 타입의 객체를 참조하는 shared_ptr에 초기화
- shared_ptr<Car> pCar( new Truck() );
- // shared_ptr<Car>가 참조하고 있던 객체를 Truck 타입으로 static_cast하여 대입.
- // 대입 하였기에 참조 카운트는 '2'
- shared_ptr<Truck> pTruck = static_pointer_cast<Truck>(pCar);
- // 위처럼 대입하지 않고 스스로 형변환만 하여도 상관없음.
- // 참조 카운트는 당연히 변화가 없다.
- static_pointer_cast<Car>(pCar);
8) 참조 객체 접근
shared_ptr이 참조하는 실제 객체를 얻는 방법은 명시적/암시적의 두 가지 방법이 있다.
명시적 방법
- shared_ptr::get()
: 참조하고 있는 객체의 주소를 반환한다.
암시적 방법
- shared_ptr::operator*
: 참조하고 있는 객체 자체를 반환한다.
: 즉, *(get())의 의미 - shared_ptr::operator->
: get()->의 의미가 같다.
예제)
- shared_ptr<Car> spCar( new Truck() );
- // spCar가 참조하는 객체의 주소를 반환
- Car* pCar = spCar.get();
- // spCar가 참조하는 객체의 메써드에 접근 #1
- spCar.get()->MemberFunc();
- // spCar가 참조하는 객체의 메써드에 접근 #2
- *(spCar).MemberFunc();
- // spCar가 참조하는 객체의 메써드에 접근 #3
- 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) 적절한 활용 예시
포인터를 담는 벡터에 대한 내용인데, 괜찮아서 링크 건다.
'프로그래밍' 카테고리의 다른 글
C++용 Microsoft 단위 테스트 프레임워크를 사용하여 C/C++용 단위 테스트 작성 (0) | 2015.04.18 |
---|---|
[스크랩]C++11: unique_ptr (0) | 2013.10.04 |
네트워크 용어 정리 (0) | 2013.06.05 |
Boost::asio – Overview – Core Concepts and Functionality – Thread and Boost.Asio (0) | 2013.05.24 |
Boost::asio – Overview – Core Concepts and Functionality – The Proactor Design Pattern: Concurrency Without Threads (0) | 2013.05.24 |