로그인

검색

STL/Boost
2012.08.02 22:34

boost::shared_ptr 소개

조회 수 802 추천 수 0 댓글 0
?

단축키

Prev이전 문서

Next다음 문서

크게 작게 위로 아래로 게시글 수정 내역 댓글로 가기 인쇄
?

단축키

Prev이전 문서

Next다음 문서

크게 작게 위로 아래로 게시글 수정 내역 댓글로 가기 인쇄

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이 되는 순간 자동으로 삭제될 것입니다.

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


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

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

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

// 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 객체는
  // 소멸될 겁니다.

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타입에 관하여)


출처 : http://yesarang.tistory.com/40

?

  1. Programming 게시판 관련

  2. 프로그래밍 관련 사이트

  3. boost::shared_ptr 소개

  4. 윈도우 프로그램의 종료 메시지 순서

  5. 동적 프로그래밍

  6. stdafx.h 사용 (미리 컴파일된 헤더)

  7. 세마포어를 이용한 생산/소비자

  8. IT 세미나 유튜브 동영상

  9. 윈도우 8 앱 개발 동영상 강의

  10. 코드 실행 시간 계산

  11. [OpenCV] 얼굴 인식 예제

  12. Dumpbin.exe 사용

  13. 특정 자료형의 데이터를 binary(hex값, 2진수값)으로 변환

  14. Base64 decoder (binary file로 저장)

  15. C++에서 base64로 인코딩

  16. Great summary cheat sheet (OpenCV)

  17. Visual Studio Debug Tips

  18. 이클립스에서 ADT 설치시 에러 해결

  19. 안드로이드 어플 개발 사이트

  20. 안드로이드 개발 참고 사이트

  21. 안드로이드 프로세스 확인

Board Pagination Prev 1 2 3 4 5 6 7 8 9 10 ... 15 Next
/ 15