C++ 이야기 네번째: boost::shared_ptr 소개

Posted at 2007.03.11 15:00 // in S/W개발/C++ 이야기 // by 김윤수


C++ 이야기 네번째입니다. 개발자가 아니신 분들이나 C++ 로 주로 개발하지 않으시는 분들은 별로 관심이 가는 내용이 아닐 것 같네요.

이번에는 auto_ptr 과 같이 Smart Pointer의 일종이지만 작동 방식이 약간 다른 boost::shared_ptr 을 소개해 드릴까 합니다.(이번에는 좀 깁니다. 각오하시고 보세요)

눈치 빠른 분들은 이름에서 감을 잡으셨겠지만, shared_ptr은 자신이 가리키고 있는 객체에 대한 참조 카운트를 유지하는 녀석입니다. 어떻게 유지하냐구요 ? 그거야 복사 동작이 일어날 때는 참조 카운트를 늘리고, 소멸이 일어날 때는 참조 카운트를 줄이는 거죠. 그러다가 참조 카운트가 0이 될 때, 그제서야 가리키고 있는 객체를 소멸시키게 됩니다. 참 똑똑한 녀석입니다.

게다가 더 좋은 점은 auto_ptr 처럼 복사할 때 이상하게 동작하는 녀석이 아니라는 겁니다. 보통 복사하는 동작이랑 거의 비슷하기 때문에 복사가 훨씬 자연스럽습니다-그렇다고 완전히 동일한 건 아닙니다. deep copy가 아니라 참조 카운트만 하나 늘리는 것이니깐요.

그럼 shared_ptr을 어떻게 쓸 수 있는지 볼까요 ?

#include "BigClass.h"
// shared_ptr을 쓰기 위해 include 해야 합니다.
#include “boost/shared_ptr.hpp”

typedef boost::shared_ptr<BigClass> BigClassSharedPtr;
void f()
{
  // auto_ptr 대신 shared_ptr을 씁니다.
  // 참조 카운트는  당연히 1로 초기화 되겠지요.
  BigClassSharedPtr bcsp1(new BigClass());
  // 이런 저런 일을 합니다.

  // bcsp1에서 bcsp2로 소유권 이전이 되지 않고,
  // 참조 카운트만 2로 늘어납니다.
  BigClassSharedPtr bcsp2(bcsp1);

  // bcsp1은 여전히 유효한 객체를 가리키고 있습니다

  // ‘->’ 로  멤버 접근도 그대로 할 수 있구요.
  int nprop = bcsp1->GetProperty();
  // 역참조 연산자 ‘*’ 를 그대로 쓸 수 있습니다.
  BigClass bc = *bcsp2;              
  ......                              // 이런 저런 일을 합니다.
} // 이 시점에서 bcsp2 와 bcsp1의 소멸자가 차례로-생성된 순서의 거꾸로-불리면서,
  // 참조 카운트가 0이 되기 때문에 shared_ptr 이 가리키는 객체도 소멸됩니다.

어떻습니까? 아름답죠? 예외 안전성도 확보하면서 메모리 새는 걱정까지 막았으니까요. 게다가 복사도 맘대로할 수 있고.

이 코드를 보시고 나더니 저기 머리 회전 빠르신 분들 새로운 사용처를 금방 찾아내셨군요. 포인터로 객체를 가리키는 멤버를 가지고 있는 클래스에서 그냥 포인터 대신 shared_ptr을 쓰면, 소멸자에서 포인터로 가리키고 있는 객체를 일일이 삭제하지 않아도 된다구요 ? 예, 똑똑하시네요. 하나를 가르쳐 주면 둘을 아는 훌륭한 학생이네요. 예를 들어, 아래와 같은 BigClass 를 좀 나이스하게 바꿔 보자는 것 같네요.

// 다음은 BigClassImpl.h 에 정의되어 있습니다.
// 실제 implementation이 정의되어 있는 클래스입니다.
class BigClassImpl {
private:
  int m_nprop;
 
public:
  BigClassImpl(int nprop = 0);
  int GetProperty();
  void SetProperty(int nprop);
};

// 다음은 BigClass.h에 정의되어 있습니다.
class BigClassImpl;

// 인터페이스가 정의되어 있는 클래스입니다.
class BigClass {
private:
  BigClassImpl* m_pImpl;
 
public:
  BigClass(int nprop = 0): m_pImpl(new BigClassImpl(nprop)) {}
  ~BigClass() { delete m_pImpl; }
  int GetProperty() { m_pImpl->GetProperty(); }
  void SetProperty(int nprop) { m_pImpl->SetProperty(nprop); }
};
 
소멸자에서 delete m_pImpl; 이라고 객체를 일일이 삭제하고 있는 걸 확인하실 수 있습니다. 그리고 이 코드는 복사 생성자와 복사 대입 연산자가 정의되어 있지 않기 때문에 이렇게 정의된 클래스 인스턴스를 이리 저리 굴려먹다가 한 번 삭제하면 그 다음부터 다른 인스턴스는 모두 껍데기만 있는 해골 바가지가 될 것 입니다. m_pImpl 멤버가 있지도 않은 객체를 가리킬 것이니까요. 자~ 다음 코드를 한 번 보세요.

#include “BigClassImpl.h”
#include “BigClass.h”
 
BigClass* f()
{
  BigClass bc(1000);
  BigClass pbc =
    new BigClass(bc);  // 값을 리턴하기 위해 bc를 가지고 복사 생성합니다.
                       // 기본 복사 생성자는 그냥 m_pImpl 포인터만 복사
                       // 하게 될 것입니다.
  ......               // 이런 저런 일을 하다가
  return pbc;          // pbc를 리턴합니다.
} // 이 시점에서 bc.m_pImpl이 가리키던 객체는
  // 저 세상으로 갑니다. 헉! 그럼 pbc->m_pImpl 이
  // 가리키던 객체는 ?

이런 복잡한 문제들을 한 방에 날려버릴 비장의 무기가 여러분에게 있으니 바로 shared_ptr 입니다. 그냥 BigClass 포인터 대신에 shared_ptr<BigClass> 를 쓰는 거죠. 여러분이 객체 소멸자에 별다른 내용을 써 주지 않으면 컴파일러는 여러분이 클래스 정의에 멤버 변수를 써준 그 역순으로 멤버 변수들의 소멸자를 호출하도록 되어 있는 것을 잘 아실 겁니다. 그 단순한 사실을 이용하는 거죠. 그렇다면 BigClass 소멸자가 호출되는 과정에서 shared_ptr<BigClass>의 소멸자도 호출되고, 그 와중에 shared_ptr<BigClass> 가 가리키던 객체도 소멸하게 될 것입니다. 그러니 애써서 BigClass 소멸자에서 일일이 신경 써 가며 delete m_pimpl을 호출할 일이 없어지는 거죠.

BigClass 정의를 이렇게 바꾸는 겁니다.

// 다음은 BigClass.h에 정의되어 있습니다.
#include “boost/shared_ptr.hpp”

class BigClassImpl;
 
// 인터페이스가 정의되어 있는 클래스입니다.
class BigClass {
private:
  typedef boost::shared_ptr<BigClassImpl> BigClassImplSharedPtr;
  BigClassImplSharedPtr m_spImpl;

public:
  BigClass(int nprop = 0): m_spImpl(new BigClassImpl(nprop)) {}
  ~BigClass() {}          // 원래 코드와 달리 delete m_spImpl 이 없습니다
  int GetProperty() { m_spImpl->GetProperty(); }
  void SetProperty(int nprop) { m_spImpl->SetProperty(nprop); }
};

게다가 아까 문제가 있던 코드도 문제가 없어집니다. 진짜로요. 여러분이 BigClass*를 shared_ptr<BigClass> 라고 바꾸는 순간 포인터와의 전쟁에서 여러분이 유리한 고지를 점령하게 되는 겁니다. 못 믿겠다고요 ? 에이 속고만 사셨나~ 정 못 믿겠다면 자세히 들여다 보기로 하죠.

#include “BigClassImpl.h”
#include “BigClass.h”
 
BigClass* f()
{
  BigClass bc(1000);
  BigClass* pbc =
    new BigClass(bc);      // 값을 리턴하기 위해 bc를 가지고 복사 생성합니다.
                           // 기본 복사 생성자는 그냥 m_spImpl 포인터를 복사
                           // 하게 되고, m_spImpl은 참조 카운트를 2로 증가
                           // 시킵니다.
  ......                   // 이런 저런 일을 하다가
  return pbc;              // pbc를 리턴합니다.
}  // 이 시점에서 bc.m_spImpl 의 소멸자가 불리면
   // 참조 카운트가 1로 됩니다. pbc->m_sImpl 이
   // 가리키던 객체는 포인터와의 전쟁에서 살아남게 됩니다.

여기에 한 술 더 떠서 f() 가 BigClass 포인터를 직접 리턴하는 것이 아니라, shared_ptr<BigClass>를 리턴하도록 할 수도 있을 겁니다.

// typedef 매직을 썼다고 가정하죠.
BigClassSharedPtr f()
{
  BigClassSharedPtr spbc(new BigClass(1000));  // 참조 카운트 1이됨 더불어
                                               // 예외 안전성 확보
  ......
  return spbc;
} // 복사 생성자로 임시 객체를 하나 생성하면서 참조 카운트가 2가 됐다가
  // 지역 변수인 spbc가 소멸되면서 다시 1이 됩니다. 문제 없이 객체가 리턴됩니다.




shared_ptr을 써 먹을 수 있는 곳이 또 있습니다. 바로 표준 컨테이너에 써 먹을 수 있습니다. 아직 표준 컨테이너에 대해 설명 드리진 않았지만 vector 정도는 다들 잘 아신다고 생각하고, 한 번 설명드려 보겠습니다.(잘 모르시는 분들을 위해 살짝 설명 드리면, vector는 알아서 줄어들었다 늘어났다 하는 편한 array 정도로 알고 계서도 될 것 같습니다)

표준 컨테이너는 모든 객체들을 집어 넣을 때 기본적으로 복사 방식으로 집어 넣도록 되어 있습니다. 따라서, 복사 연산 비용이 클 수 밖에 없는 덩치 큰 객체를 vector 에 담고 싶을 경우에는 보통 그 객체의 포인터를 원소로 갖는 vector를 생각하기 마련입니다. 예를 들어, BigClass 가 이름에 걸맞게 정말로 덩치가 큰 녀석이라고 생각해 봅시다. 그래서 다음과 같이 vector를 정의해서 쓰려고 합니다.

// vector를 쓰려면 include 해야 합니다.
#include <vector>
#include "BigClass.h"
#include "BigClassImpl.h"

typedef std::vector<BigClass*> BigClassPtrVector;

위와 같이 정의해 놓고 쓴다면, vector를 삭제할 때는 반드시 각각의 원소들을 미리 삭제를 해줘야 되겠지요.

void f()
{
  BigClassPtrVector vbc;

  for (int i = 0; i < 10; i++)
  {
   // 중간 쯤 원소를 집어 넣다가 예외가 발생하면 ???
    vbc.push_back(new BigClass(i));
  }
  ......                 // 이런 저런 일을 한다고 칩니다
                         // 이런 저런 일을 하다 중간에 예외가 발생하면 ???

  // 마지막에 vector에 담겨있는 원소들이 필요 없게 된다면 이렇게 일일이
  // 삭제해야 합니다.
  // 이렇게 index를 쓰지 않고 iterator라는 걸 쓰는 방법도 있지만, iterator를
  // 설명하려면 다시 복잡해 지니까 그냥 index 쓰는 예제로 보여 드립니다.
  for (int i = 0; i < 10; i++)
  {
    delete vbc[i];
  }
}

위 코드도 조금만 들여다 보면 예외가 생겼을 때, 메모리가 줄줄 새게 된다는 걸 눈치 채셨을 겁니다. 꼭 예외뿐만이 아니더라도 중간에 에러가 발생해서 에러 처리를 하면서 항상 자원을 해제하는 코드가 들어가야 한다면 에러 처리 코드가 하염없이 길어질 수 있습니다. 결코 바람직한 방법은 아닙니다. 이런 문제를 해결하기 위해 shared_ptr을 사용할 수 있습니다. 어차피 vector 도 자신이 소멸되기 전에 각 원소를 소멸시키기 때문에 각 원소에 포인터 대신 shared_ptr을 둔다면 share_ptr이 가리키는 객체는 참조 카운트가 0이 되는 순간 자동으로 삭제될 것입니다.

위의 코드를 다음과 같이 바꾸면 새는 메모리를 꽉꽉 막을 수  있게 됩니다.

// vector를 쓰려면 include 해야 합니다.
#include <vector>
#include "boost/shared_ptr.hpp"
#include "BigClass.h"
#include "BigClassImpl.h"

typedef boost::shared_ptr<BigClass> BigClassSharedPtr;
typedef std::vector<BigClassSharedPtr> BigClassSharedPtrVector;

void f()
{
  BigClassSharedPtrVector vspbc;

  for (int i = 0; i < 10; i++)
  {
    BigClassSharedPtr spbc(new BigClass(i));
    vbc.push_back(spbc);   // 중간 쯤 원소를 집어 넣다가 예외가 발생하면 ???
  }
  ......                   // 이런 저런 일을 한다고 칩니다
                           // 이런 저런 일을 하다 중간에 예외가 발생하면 ???
} // 마지막에 vector에 담겨있는 원소들이 필요 없게 된다면 이렇게 일일이
  // 삭제할 필요가 없습니다. vector의 소멸자가 불리면서 vector 각 원소의 소멸자도
  // 불리게 되고, 그러면 BigClassSharedPtr 각 원소가 가리키는 BigClass 객체는
  // 소멸될 겁니다.

위 코드는 중간에 예외가 발생하더라도 stack unwinding이 일어나면서 vector의 소멸자가 호출되고, 다시 BigClassSharedPtr 의 소멸자가 호출되므로 메모리가 새는 일이 없습니다.

이 정도면 한 번 쓸만하지 않나요 ? 당장 한 번 여러분이 짠 코드를 들여다 보세요. 메모리가 새고 있지는 않은지, 새지는 않더라도 새는 걸 막느라고 얼마나 많은 에러 처리 코드를 얼기 설기 짜 놓았는지.

그렇다고 shared_ptr 를 여기 저기 아무 생각 없이 쓸 수 있을까요 ? 그건 당연히 아니죠. shared_ptr에 의해 가리키는 객체는 여러 shared_ptr에 의해 공유가 되고 있기 때문에 만약 한쪽에서 수정을 하면 다른쪽에서도 그 수정된 내용이 공유가 될 것입니다.  만약 이런 것이 허용되지 않는 경우에는 shared_ptr을 쓰면 안 되겠죠. 그리고, shared_ptr이 서로 circle을 이루어서 가리키게 되는 경우에는 절대 참조 카운트가 0이 되지 않기 때문에 써서는 안된다고 하네요.(이렇게 말하긴 하는데 어떻게 하면 그렇게 circle이 발생하는지 예제를 생각해 내기가 어렵네요. 아시는 분 좀 알려주세요) circle을 이룰 수 있는 경우에는 boost::weak_ptr<>을 쓰면 된다고 하는 군요.

shared_ptr 앞에 왜 boost 라는 namespace가 붙어 있을까 궁금해 하시는 분들을 위해 boost에 대해 잠깐 소개드리겠습니다.

boost는 일종의 확장 C++ 라이브러리인데, 상당히 많은 C++ 전문가들의 리뷰를 거쳐 탄생한 상당히 탄탄한 라이브러리라고 생각하시면 됩니다. 이 라이브러리의 워낙 정의 및 구현이 잘 되어 있어서 그런지 최근에 일부 클래스들이 C++ 표준화 위원회의 Library Technical Report 에 채택되기도 했습니다-shared_ptr도 std::tr1 이라는 namespace에 채택이 되었습니다. 다음과 같은 영역에서 상당히 많은 기능이 지원되고 있습니다.

String and text processing
Containers
Iterators
Algorithms
Function Objects and higher-order programming
Generic Programming
Template Metaprogramming
Preprocessor Metaprogramming
Concurrent Programming
Math and numerics
Correctness and testing
Data structures
Input/Output
Inter-language support
Memory
Parsing
Programming Interfaces
 

홈페이지는 http:://www.boost.org 이니 관심 있으신 분들은 가벼운 마음으로 들러 보세요.

...
...
...

(제가 한 가지 거짓말을 했는데요. www.boost.org 는 절대 가벼운 마음으로 들러 볼 곳은 아닌 것 같아요. 솔직히 거기 있는 내용을 이해하려면 상당한 내공이 이미 갖춰져 있어야 하는 듯...)

여기까지 왔는데 다음에는 뭘 할까요 ? 고민 고민 ^^;

TODO:
- shared_ptr의 인터페이스 및 구현 설명
- boost::scoped_ptr & boost::noncopyable 설명

참고문헌
Effective C++ 3rd Edition, Scott Meyers, 항목 13, 14
boost shared_ptr class template(클릭해 보세요)

소프트웨어 관련된 저의 다른 글들도 참고로 읽어 보세요.

소프트웨어는 soft 해야 제 맛이다
Flexible한 S/W 작성하기
소스코드 복사의 위험성
C++ 이야기 첫번째: auto_ptr 템플릿 클래스 소개
C++ 이야기 두번째: auto_ptr의 두 얼굴
C++ 이야기 세번째: new와 delete
C++ 이야기 네번째: boost::shared_ptr 소개
C++ 이야기 다섯번째: 내 객체 복사하지마!
C++ 이야기 여섯번째: 기본기 다지기(bool타입에 관하여)


제 글이 유익하셨다면 오른쪽 버튼을 눌러 제 블로그를 구독하세요. ->
블로그를 구독하는 방법을 잘 모르시는 분은 2. RSS 활용을 클릭하세요.
RSS에 대해 잘 모르시는 분은 1. RSS란 무엇인가를 클릭하세요.

신고
  1. 형기의 자료공간

    2007.03.11 18:02 신고 [수정/삭제] [답글]

    boost의 shared_ptr은 현재 C++ 표준화위원회에 의해
    TR1(Technical Report 1)에 포함되었습니다. (EC++ 3판)
    앞으로는 표준이 되겠죠..
    현재 TR2 작업이 진행중입니다. 이런것들을 포함한.. C++ 2.0표준이 2008년 쯤에 나온다니.. 기대해 봐도 될 것 같습니다.

    Modern C++ Design에는 loki 라이브러리에서 SmartPtr이란 템플릿 클래서에서 shared_ptr와 동일한 역할을 하는 녀석이 있구요..
    codeguru나 codeproject에 보면 shared_ptr와 동일한 기능을 하는 녀석들이 많이 있습니다.

    상황에 따라 적절한 녀석으로 사용해야 할 것 같네요..

    • 김윤수

      2007.03.11 18:26 신고 [수정/삭제]

      예, 저도 알고 있긴 했는데 글에 반영하진 않았습니다. 좋은 정보 감사합니다.

    • 형기의 자료공간

      2007.03.11 19:03 신고 [수정/삭제]

      boost가 강력하긴 하지만, 실은 제가 boost를 즐겨쓰진 않습니다. 그래서, 다른 것을 소개한 것입니다.

      물론, regex와 같은 훌륭한 기능이 있긴 하지만..
      말리 tr1이 visual c++와 같은 정규컴파일러에 포함되길 기다리고 있습니다. ^^;;

      boost의 메타프로그래밍과 regex등은 그 강력함 때문에 테스트로 사용해 보기는 했죠..

      그러나, 실 프로젝트에서는 boost보단 용도에 맞는 다른 적절한 클래스를 사용하곤 합니다. ^^;;

      그럼, 즐거운 한주 되세요..

  2. 강석민

    2007.06.12 09:05 신고 [수정/삭제] [답글]

    훌륭하신 글 잘 읽어 보았습니다..좋은 내용이 무척 많군요..

    shared_ptr의 cycle 현상은 아래 간단하게 확인 하실수 있습니다.

    #pragma warning(disable:4819)
    #include <iostream>
    #include <memory>
    using namespace std;
    using namespace std::tr1;

    struct node
    {
    shared_ptr<node> next;
    ~node() { cout << "~node" << endl;}
    };

    void cycle()
    {
    node* head = new node;
    node* N1 = new node;

    shared_ptr<node> root( head );
    head->next = shared_ptr<node>(N1);
    N1->next = root; // 이순간cycle이발생합니다.
    }

    int main()
    {
    cycle();
    return 0;
    }


    node안에 있는 next를 weak_ptr로 수정하면 메모리 leak를 잡을수 있습니다.

    그럼.. 앞으로도 좋은 글 부탁 드립니다.

  3. 강석민

    2007.06.12 09:09 신고 [수정/삭제] [답글]

    참고로 위소스는 boost-1.3.4 버전과 vc++2005 express 버전를 사용했습니다
    pragma 지시어는 문자 format이 달라서 나오는 warning 때문에 추가 했구요..

    글 적을때 비밀 번호를 뭐라 했는지 기억이 나지 않는군요..그래서 수정을 못하고 추가 댓글로.. 블로그를 지저분하게 해서 죄송하군요.

  4. 부스트초보

    2007.08.30 21:32 신고 [수정/삭제] [답글]

    알려주신 boost 를 보다가 궁금한 점이 생겼는데요, 왜 boost 는 헤더 화일 말고도 cpp파일을 만들었나요?
    STL 처럼 헤더화일로만 존재하였으면 더 좋았을텐데...
    제가 초보라서 너무 무식한 질문을 한것인가요? 기술적으로 cpp파일이 꼭 필요한 경우가 있는것인가요?
    궁금하네요.. 고수님께서 알려주시면 감사하겠습니다~ ^^

  5. 김윤수

    2007.08.31 01:29 신고 [수정/삭제] [답글]

    기능에 따라서 cpp 로 구현을 할 필요가 있는 것들이 있다라는 정도로만 알고 있습니다. boost 기능은 모두 template 기능은 아닙니다. 상당히 방대한 기능이 포함되어 있고 기능에 따라서는 cpp 에 implementation 해야하는 것들이 있습니다. 예를 들어 regular expression 기능은 같은 것들이요.

  6. 정헌

    2014.03.29 02:39 신고 [수정/삭제] [답글]

    안녕하세요

    검색하면서 자주 들리다 보니 즐겨찾기가 아닌 즐겨찾기가 된 느낌이네요
    설명을 참 잘해주셔서 잘 보고 갑니다. 고맙습니다 ~

  7. 권성욱

    2017.06.28 03:04 신고 [수정/삭제] [답글]

    자주 들리겠습니다_ 잘 배우고 갑니다.

댓글을 남겨주세요.