boost::shared_ptr에 관해!

boost::shared_ptr은 boost/smart_ptr.hpp를 이용해 사용할 수 있는 라이브러리입니다.

smart_ptr은 그 이름과 같이, smart pointer들을 다루고 있는 라이브러리로, noncopyable(복사 불가능한) auto_ptr인 scoped_ptr과 레퍼런스 카운팅을 이용해 객체 해제를 자동으로 관리해주는 shared_ptr, shared_ptr에 레퍼런스 카운팅을 하지 않고 단순히 참조만 할 수 있게 해주는 weak_ptr, shared_ptr과 유사한 intrusive_ptr이 있으며, 배열 스타일의 포인터 사용을 위한 scoped_array, shared_array 역시 존재합니다.

지금부터 알아볼 클래스는 바로 boost::shared_ptr로 C++ 프로그래머들이 겪는 Memory Leak을 효과적으로 방지하고, 동적으로 할당한 객체들을 컨테이너에서 손쉽게 관리하도록 해주는 매우 유용한 툴입니다.


1. std::auto_ptr의 함정.

뜬금없이 왠 std::auto_ptr을 언급하냐! 라며 울부짖으실 분도 있으시겠지만, 많은 사람들이 저지르는 실수 중 한가지가 auto_ptr과 관련이 있으며, 이에 대한 해결책 중에 한가지가 shared_ptr이기에 짚고 넘어가려고 합니다.

Scott Meyers의 Effective STL 항목8에 잘 나와있듯이, auto_ptr의 컨테이너는 절대로 만들어서는 안됩니다. 오래 살고 싶으시다면요. ^^

먼저 auto_ptr에 대해 잠깐 알아보기로 하지요.

#include <memory>

void test()
{
//먼저 힙에 int를 하나 만듭시다.
int* int_p = new int;
//auto_ptr_int을 만듭니다! 녀석은 int_p를 관리하게 됩니다.
std::auto_ptr<int> auto_ptr_int(int_p);
*auto_ptr_int = 3; //  int_p에 3을 넣습니다.
std::cout << "hehehe in test()" << std::endl;
// 그냥 심심해서 뿌려본겁니다.
return; // 이때 int_p가 파괴됩니다.
}
&#91;/cpp&#93;

위의 예제에서 알 수 있듯이, int_p는 함수가 종료됨과 함께 파괴됩니다. 그 이유는 auto_ptr때문이지요.

auto_ptr은 자신이 파괴될 때, 자신이 소유한 객체를 파괴합니다. 예제에서는 스택에 auto_ptr을 생성했으므로, 스택이 파괴될때 - 즉, 함수가 종료될 때 - auto_ptr이 파괴됩니다.
소유권 이전의 문제도 알아봅시다.

&#91;cpp&#93;
#include <memory>
void test2()
{
std::auto_ptr<int> ap1;
ap1 = new int; // 새로 생성된 int객체에 대한 소유권은 ap1에 있습니다.
{
std::auto_ptr<int> ap2;
ap2 = ap1; // ap1이 갖고 있던 소유권이 ap2에 이전됩니다.
}
// 블럭이 종료되면서 ap2가 파괴됩니다.
// 따라서, int객체도 파괴되지요.
}
// ap1이 파괴되지만, ap1은 어떤 객체도 소유하고 있지 않으므로
// 아무일도 일어나지 않습니다.

auto_ptr은 위에서 알 수 있듯이, 소유권을 공유할 수 있는 방법은 없습니다. 강제적으로 하더라도, 같은 객체를 소유하고 있는 auto_ptr중 어느 하나라도 파괴된다면, 그 즉시 객체가 파괴되어 버리므로, 프로그램은 엉망이 되어버리겠지요.

auto_ptr의 컨테이너를 만들면 안되는 이유도 같은 이유입니다.

#include <memory>
#include <vector>

// C++ 프로그래머의 영원한 친구 typedef!!!!
typedef std::auto_ptr<int> AIP;
void test3()
{
std::vector<aip> aip_vec;
....(aip_vec에 데이터를 마구마구 넣읍시다.)...
AIP ap1 = aip_vec.at(0);
AIP ap2 = aip_vec.at(0);
std::cout << *ap1 << std::endl; // 문제되지 않습니다.
std::cout << *ap2 << std::endl; // segment fault가 발생합니다.
}
&#91;/cpp&#93;

위의 예제에서 ap1은 aip_vec의 첫번째 AIP(std::auto_ptr<int>)가 가진 int 객체에 대한 소유권을 이전받습니다. 즉 aip_vec[0]는 아무 객체도 소유하고 있지 않지요. 따라서 ap2역시 어떤 소유권도 가질 수 없습니다. 이때 ap2를 참조하게 된다면 문제가 발생 하지요.

컨테이너들은 무조건 값에 의해 작동합니다. 참조나 포인터가 아니지요. 따라서, auto_ptr을 컨테이너에 사용하는 것은 매우 위험할 수 있습니다. 알고리즘이나 멤버함수에 따라 객체가 파괴되어버릴 수 있으니까요!!!

<hr/>
<strong>2. 그렇다면 boost::shared_ptr은?</strong>

boost::shared_ptr은 레퍼런스 카운팅을 이용해 작동합니다. 레퍼런스 카운팅이란, 특정 객체에 대해 카운터를 놓고 참조자가 늘어날 때마다 카운트를 올리고, 참조자가 줄어들때마다 카운트를 내리다가, 참조자가 없어지면 그때 객체를 파괴하는 방식입니다.

앞의 test2를 shared_ptr을 이용해 바꾸고 그 동작을 살펴보기로 하지요.

[cpp]
#include <boost/shared_ptr.hpp>
void test4()
{
boost::shared_ptr<int> sp1;
// 카운트가 1이 됩니다. int객체에 대한 shared_ptr은 sp1뿐이니까요.
sp1 = boost::shared_ptr<int>(new int);
{
boost::shared_ptr<int> sp2;
sp2 = sp1; // int객체에 대한 카운트가 2가 됩니다. sp1, sp2 2개니까요.
}
// sp2가 파괴되면서 카운트가 하나 줄어듭니다. 이젠 1이군요.
}
// sp1이 파괴되면서 카운트가 줄어들어 0이 됩니다.
// 이때 int객체가 파괴됩니다. :)

간단하지요? 카운트가 늘어나고 줄어들고, 0이되면 파괴된다는 사실만 기억하시면 됩니다.
그럼 이번엔 좀 더 복잡한 예를 살펴보기로 하지요. vector를 이용한 예제입니다.

#include <boost/shared_ptr.hpp>
typedef boost::shared_ptr<int> SIP
void test5()
{
SIP sp1 = SIP(new int); // 카운트는 1입니다.
std::vector<sip> sip_vec;
sip_vec.push_back(sp1); // 카운트는 2
sip_vec.push_back(sp1); // 카운트는 3
sip_vec.pop_back(); // 카운트는 2
sip_vec.pop_back(); // 카운트는 1
} // 이때 파괴됩니다.

vector는 값에 의한 의미론이므로, push_back 멤버함수는 복사를 통해 작동하게 됩니다. 즉, sip_vec.push_back(sp1)은 sp1의 사본을 sip_vec에 넣는다 라는 의미이지요. sp1은 복사되면서 카운트를 증가시킵니다. -복사연산자, 복사생성자를 통해서요.- 따라서 push_back이 완료되고 나면 카운트는 2가 되지요.

shared_ptr의 강력함중 하나는 컨테이너에서 객체를 삭제할 경우입니다. shared_ptr을 사용하지 않고, 그냥 포인터를 써보기로 하지요.

#include <vector>
class Test; // 있다고 칩시다.
typedef std::vector<test*> TestVectorPTR;
void test6()
{
Test* t = new Test;
TestVectorPTR v;
v.push_back(t);
v.pop_back();
// t가 지워지질 않습니다아..
// 허허. 당연하죠. vector는 값의미론이니까요.
delete t; // 항상 이렇게 손으로 지워줘야 합니다.
}

객체에 대한 포인터를 여러 컨테이너에 넣어두었다고 생각해 봅시다. 그렇다면, 각 컨테이너마다 해당하는 포인터가 존재하는지 죄다 확인해가면서 삭제해야하는 우울함이 발생합니다. 흐흑.

shared_ptr을 써보면..

#include<vector>
#include <boost/shared_ptr.hpp>
class Test; // 역시나 있다고 칩시다.
typedef boost::shared_ptr<test> TSP;
typedef std::vector<tsp> TSPVector;
TSPVector vec;
void test7_init()
{
// vec에 새 객체를 추가합니다. 카운트는 1이지요.
vec.push_back(TSP(new Test));
}

void test7()
{
// init()에서 추가한 객체가 파괴됩니다. 카운트가 0가 되니까요.
vec.pop_back();
}

직접 지워주지 않아도 shared_ptr의 파괴자에서 알아서 객체를 파괴시켜줍니다. 지긋지긋한 메모리누수현상에서 해방이지요. 하핫. 여러 컨테이너에 넣는 경우도 전혀 우울하지 않습니다. 한군데서 지운다고 다른 컨테이너에서 문제가 발생하진 않을테니까요. 카운트가 0가 되는 시점에서 파괴된다는 컨셉은 메모리관리에서 아주 행복한 컨셉이지요.


3. shared_ptr의 파괴동작.

shared_ptr을 단순히 new/delete를 이용해 생성/파괴하는 객체에만 사용할 수 있다는 생각은 크나큰 오산입니다. Custom Destructor를 세팅할 수 있게 해주는 기능은 shared_ptr의 가능성을 대폭 증가시켜 줍니다. 🙂

#include <boost/shared_ptr.hpp>
// 사실 싫어하는 예이지만, HANDLE을 예로 들고 싶기에 어쩔수.. 없;;
#include <windows.h>
typedef boost::shared_ptr<void> HANDLESP;

void test8()
{
HANDLE h = ...(뭐든 핸들을 받아온다)...
HANDLESP hsp = HANDLESP(h, CloseHandle);
}// hsp가 파괴될때 CloseHandle(h)가 호출된다.

hsp를 생성할때 보면, 뒤쪽에 CloseHandle이란 함수를 넣어주는 것을 발견할 수 있습니다. CloseHandle의 위치에는 HANDLE(정확히는 void*)을 매개변수로 받는 호출가능한 C++객체는 무엇이든 올 수 있습니다. 🙂

즉, 객체가 C++ 표준인 new/delete를 이용해 할당되지 않더라도 파괴될때 호출할 호출가능한 객체를 지정해주면, delete대신 그 함수를 통해 객체를 파괴하게 되지요.

DLL 사이에서 객체를 주고 받을 때도 매우 유용합니다. DLL A에서 생성한 객체를 DLL B에서 파괴할 경우 문제가 발생하기 때문에, A의 인터페이스에 객체를 삭제하는 함수를 등록시켜서 쓰는 것이 일반적인데, 이런 경우에도 객체를 삭제하는 함수를 파괴시 호출할 함수로 지정해주면 간단히 shared_ptr을 적용할 수 있는 것이지요. 이때, 전에 설명했던 boost::bind가 큰 힘을 발휘하는 경우가 많답니다. 🙂


4. Tip.

shared_ptr이 가진 좋은 점은, C++ 프로그래머를 할당/해지, 혹은 생성/파괴 관리라는 리소스 누수현상에서 해방시켜준다는 점입니다. 상당히 행복하지요. 🙂
조금 더 행복해 지기 위해서 간단한 팁을 하나 준비했습니다.

class A
{
...(some definitions)...
public:
typedef boost::shared_ptr<a> SP;
}

정말 간단합니다. class를 하나 만들때마다 SP라는 이름으로 shared_ptr을 미리 정의해 주는 것인데요, 매번 boost::shared_ptr<타입이름>을 치기 귀찮으니 미리미리 typedef을 해두자는 간단한 작전입니다. 일단 해보면 실제 작업할때 상당히 편하기도 하고, 타입이름*보단 타입이름::SP를 통해 좀 더 안전한 코드를 미리 작성하게 되는 부수효과도 있답니다-


A. References

shared_ptr 문서
Effective STL, Scott Meyers 저/곽용재 편역, Addison Wesley / 인포북


B. notes

2009년 5월 31일: 워드프레스에 맞게 재편집.
2009년 11월 25일: 스크롤바 문제로 재편집.