C++ 이야기 첫번째: auto_ptr 템플릿 클래스 소개

Posted at 2007. 3. 4. 04:27 // in S/W개발/C++ 이야기 // by 김윤수


[이글의 최신 Update 문서는 항상 여기에서 확인할 수 있습니다]

C++ 관련 글을 연재로 써볼까 합니다. 개발자가 아니신 분들이나 C++ 로 주로 개발하지 않으시는 분들은 별로 관심이 가는 내용이 아닐 것 같네요.

첫 글로 C++ Smart Pointer 중의 하나인 auto_ptr 템플릿 클래스에 대해 소개하겠습니다.

본격적으로 소개하기 전에 다음 코드를 한 번 보시죠.

class BigClass {
private:
  int m_nprop;

public:
  int GetProperty();
};

void f()
{
  BigClass* pbc = new BigClass();   // argument는 그냥 없다고 가정합니다.
  ......                            // 이런 저런 일을 한다고 치구요.
  delete pbc;
}

위 코드의 위험성이 느껴지시나요 ? 느껴지신다면 대단한 프로그래밍 내공을 가지신 것입니다. 느껴지지 않는다 해도 너무 실망하지 마시구요. 제가 알려드릴테니.

중간에 이런 저런 일을 하는 동안 에러가 발생했는데, 리턴하기 전에 delete pbc를 빼먹거나 예외(exception)가 발생해서 delete pbc를 실행하지 못한 채로 중간에 함수 수행이 중단된다면 어떻게 될까요 ? 당연히 메모리가 샐 것입니다. 이런 문제를 해결하기 위해 가장 쉽게 생각할 수 있는 방법은 아무래도 다음과 같은 것이겠죠.

void f()
{
  BigClass* pbc = new BigClass();
  try {                // 이런 저런 일을 하는 부분을 try catch로 묶습니다
    ......
  }
  catch (...) {
    delete pbc;        // 객체를 없애고,
    throw;             // 예외를 전달
  }
  delete pbc;          // 정상적인 흐름에서 객체를 삭제
}

왠지 좀 구리지 않나요 ? 우선 제 눈에 제일 먼저 들어오는 것은 delete pbc가 두 번 반복된다는 거네요. f() 에서 동적 할당하는 객체가 pbc 하나밖에 없으니까 그렇지 두 개, 세 개씩으로 늘어나 거나 예외 종류별로 다른 로직으로 처리해야 한다면... 끔찍하겠죠. f()에서 실제로 하는 일은 아주 작은 코드이고, 대부분은 예외 처리 코드인, 배보다 배꼽이 큰 상황이 벌어질 것 입니다. 사실 이런 식의 코드는 에러를 유발할 가능성이 크기 때문에 유지 보수하는 데도 별로 좋지 않습니다. 코드를 이해하기도 쉽지 않구요.

auto_ptr은 이런 문제를 해결하기 위해 설계된 템플릿 클래스입니다. auto_ptr은 자신이 가리키고 있는 객체에 대한 "유일한" 소유자로서 작동합니다.(일단 소유자라는 말에 집중하시기 바랍니다. "유일한"의 의미는 나중에 말씀드리겠습니다) 바꿔 말하면, auto_ptr은 자신이 파괴될 때, 자신이 가리키고 있던 객체도 파괴합니다. 게다가 auto_ptr은 일반 포인터를 쓰는 것과 거의 동일하게 사용할 수 있습니다. 즉, '*' 연산자로 역참조를 할 수 있구요, '->' 연산자로는 멤버 변수나 멤버 함수를 접근할 수 있습니다. 다음과 같은 코드가 가능하다는 겁니다.

std::auto_ptr<BigClass> bcap(new BigClass());
BigClass bc = *bcap;
int nprop = bcap->GetProperty();

그렇다면 위에서 구리게 짠 코드는 어떻게 바꿀 수 있을까요 ? 벌써 눈치 채셨다구요. 예~ 훌륭하십니다.

// auto_ptr을 사용하려면 include 해야 합니다.
#include <memory>

void f()
{
  // RAII(Resource Acquisition Is Intialization) idiom
  std::auto_ptr<BigClass> bcap(new BigClass());
  ......
} // 함수를 떠나기 전에 auto_ptr 지역 변수에 대한
  // 소멸자가 호출되면서 new BigClass()로 할당한 객체가 해제됩니다.





어때요? 깔끔하죠? 코드까지 깔끔해진데다가 예외 안전성까지 확보했으니 auto_ptr을 안 쓰고는 못 배기실 겁니다. 음... 근데 저기 실눈을 뜨고 보고 계시는 분이 있군요. 아~ 지금 쓰고 있는 컴파일러가 워낙 꼬져서 템플릿을 지원하지 않는다구요. "그럼 컴파일러를 바꾸세요"라고 말하면 아마 돌이 날아 오겠죠 ? 뭐 어쩔 수 없죠. 우리네 인생이 그런 것 아니겠습니까? 컴파일러가 고생하는 대신 여러분 손이 고생해야죠. 다음과 같이 auto_ptr과 비슷한 클래스를 직접 작성하시면 됩니다.

class BigClassAutoPtr {
private:
  BigClass* m_pbc;

public:
  explicit BigClassAutoPtr(BigClass* pbc = 0): m_pbc(pbc) {}
  ~BigClassAutoPtr() { delete m_pbc; }
  BigClass& operator*() { return *m_pbc; }
  BigClass* operator->() { return m_pbc; }
}

물론 위 정의는 아직 문제가 많습니다. 바로 컴파일러가 자동으로 생성하는 복사 생성자와 복사 대입 연산자(copy assignment operator) 때문에 발생하는 문제입니다. 컴파일러가 자동으로 생성하는 복사 생성자와 복사 대입 연산자는 public 인터페이스에 속하고, member-wise copy로 동작하게 되어 있거든요. 구체적으로 어떻게 문제가 발생하는지 어떻게 auto_ptr에서는 해결하고 있는지는 다음 기회에 말씀 드리겠습니다.

마지막으로 템플릿을 지원하는 환경에서 템플릿을 지원하지 않는 환경으로의 이식을 생각해서 코드를 작성하는 법에 대해 잠깐 언급하고 이번 이야기를 마칠까 합니다. 그 비법은 바로 typedef 마술을 사용하는 것입니다.

// BigClass.h에 다음과 같이 정의하세요
#ifdef TEMPLATE_SUPPORTED
#include <memory>
#endif

class BigClass { ...... };

#ifdef TEMPLATE_SUPPORTED
typedef std::auto_ptr<BigClass> BigClassAutoPtr;
#else
class BigClassAutoPtr { ...... };
#endif

// BigClass를 사용하는 쪽에서는 다음과 같이 작성하세요.
#include "BigClass.h"

void f()
{
  BigClassAutoPtr bcap(new BigClass());
  ......
}

다 알고 계셨다구요 ? 예~ 당연히 그러시겠죠.

다 읽어 보신 후에 잘못된 부분이나 추가할 부분이 있으면 알려주세요. 질문도 좋구요.

참고 문헌
C++ Standard Library: A Tutorial and Reference 4.2 auto_ptr class,Nicolai Josuttis 저
Effective C++ 3rd Edition, Chapter 3 Resource Management, Scott Meyers 저

(근데 새벽 3시에 깨서 뭐하고 있는 건지 모르겠네요... 갑자기 깼는데 잠이 안 와서리...)

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

소프트웨어는 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란 무엇인가를 클릭하세요.