C++ 이야기 두번째: auto_ptr의 두 얼굴

Posted at 2007. 3. 6. 18:54 // in S/W개발/C++ 이야기 // by 김윤수


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

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

저번 글을 통해서 auto_ptr에 대해서 소개는 드렸으니 이제 auto_ptr이 어떤 녀석인지 더 알아보기로 하겠습니다. 제 목표는 여러분이 auto_ptr 연인이 돼서 열심히 부비대는 것이니까요.

지난 글에서 마지막쯤에 다음 BigClassAutoPtr은 문제가 있다고 한 것 기억하시나요 ?

class BigClassAutoPtr {
private:
  BigClass* m_pbc;

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

C++ 컴파일러는 클래스 정의에 복사 생성자와 복사 대입 연산자(copy assignment operator)가 포함되어 있지 않으면 member-wise copy를 수행하는 복사 생성자와 복사 대입 연산자(copy
assignment operator) 를 지가 알아서 만들어 버립니다. 지딴에는 똑똑한 척 하는 거죠. 뭐~ 상당히많은 경우에는 컴파일러가 알아서 만드는 게 의도한 것과 딱 맞을 수 있지만, 또 상당히 많은 경우에 문제가 되기도 하죠. 문제가 되는 대표적인 경우가 바로 member로 pointer를 가지고 있는 경우입니다.

자~ 다음 코드를 한 번 보시죠. 무슨 일이 벌어질까요 ?

BigClassAutoPtr f()
{
  // 이전과 같이 RAII로 깔끔하게 초기화
  BigClassAutoPtr bcap(new BigClass());
  ......              // 이런 저런 일을 하구요
  return bcap;
}  // 이 시점에서 bcap 이 임시 객체로 복사가 되고
  // (값에 의한 리턴이니까) bcap은 소멸되면서
  // bcap이 가리키던 객체도 소멸됩니다.

void g()
{
  ......
  BigClassAutoPtr p = f();   // 임시객체를 다시 p에 복사
  p->GetProperty();
}

어떤 일이 벌어질지는 눈에 선하시죠 ? p->GetProperty() 를 수행할 때, 유식한 말로 해서 undefined behavior가 일어납니다. f()를 리턴하는 시점에서 값에 의한 리턴 규칙에 따라 컴파일러가 생성한 복사 생성자는 임시객체.m_pbc = bcap.m_pbc로 만들어 버리고, bcap의 소멸자는 bcap이 가리키고 있던 BigClass 객체-사실 임시객체도 같은 객체를 가리키고 있었습니다-를 소멸시켜 버립니다. 그렇다면 g()안에서 p는 p = f()가 끝난 이후로는 껍데기만 남은 시체가 되버린다는 것이죠. 간담이 서늘하실 겁니다.

오호라! 그렇다면 이 난관을 어떻게 극복해야 한답니까? 잠깐 생각해 보시죠. 여러분의 내공이라면
금방 몇 가지 방안이 생각나실 겁니다.

1. 복사를 원천적으로 막아 버린다 --> boost::scoped_ptr<> 이 쓰는 방식입니다.
    boost::scoped_ptr<> 에 대해서는 나중에 소개하도록 하겠습니다. 이 방법은 사용자가
    실수로 복사해서 발생하는 문제를 원천적으로 막을 수 있기 때문에 꽤나 쓸모 있습니다.
2. 가리키는 객체에 대해 참조 카운팅을 수행합니다
    --> boost::shared_ptr<>이 쓰는 방식입니다.
    boost::shared_ptr<> 에 대해서는 나중에 소개하도록 하겠습니다.
3. 관리하고 있는 객체를 진짜로 복사합니다. 소위 deep copy 라고도 하죠.
4. 복사할 때 관리하고 있는 객체의 소유권을 옮겨 버립니다. --> 바로 auto_ptr<>이
    쓰는 방식입니다.

auto_ptr의 이러한 작동 방식을 유식한 말로 ownership transfer 라고 합니다. 이런 작동 방식 때문에 "C++ 이야기 첫번째: auto_ptr 템플릿 클래스 소개"에서 "유일한" 소유자라고 소개를 했었습니다.

이 개념을 코드로 옮기면

class BigClassAutoPtr {
private:
  BigClass* m_pbc;

public:
  ......
  // 복사 생성자
  BigClassAutoPtr(BigClassAutoPtr& other)
  {
    delete m_pbc;
    m_pbc = other.m_pbc;
    other.m_pbc = 0;
  }
  // 복사 대입 연산자
  BigClassAutoPtr& operator =(BigClassAutoPtr& other)
  {
    delete m_pbc;
    m_pbc = other.m_pbc;
    other.m_pbc = 0;
  }
  // utility method로 get()을 정의
  BigClassAutoPtr* get() { return m_pbc; }
}

이렇게 됩니다. 쬐금 이상할 것입니다. 이 쬐금 이상한 것 때문에 good news 와 bad news가 생겼답니다. good news와 bad news 중에 어떤 걸 먼저 얘기해 드릴까요 ? 매도 먼저 맞는 게 낫다고, bad news부터 보시죠.





// 값에 의한 전달입니다. 여기서 복사 생성자가 호출되
// 면서 p가 객체에 대한 소유권을 갖습니다.

void BigClassPrint(BigClassAutoPtr p)  
{
  if (p.get() == 0)
  {
    std::cout << "NULL";
  }
  else
  {
    std::cout << *p;
  }
}            // 여기서 다시 p 가 소멸되면서 p가 가리키는 객체도 소멸

아직까지는 bad news를 못 보시겠다구요 ? 그럼 이건 어떤가요 ?

BigClass bc;
BigClassAutoPtr ap(new BigClass());
BigClassPrint(ap);       // ap의 소유권이 BigClassPrint()에게 넘어감
*ap = bc;                // Oops!!

ownership transfer 개념이 익숙하지 않기 때문에 얼마든지 위와 같이 잘못된 코드를 짤 수 있습니다. 다시 말하지만 말 그대로 소유권이 이전됩니다. 남에게 소유권을 이전하고서는 자꾸 자기가 다시 쓰려고 하면 당연히 법에 어긋나겠지요. "auto_ptr을 복사하면 소유권 이전 등기를 한 것이다"라고 생각하시면 됩니다. 소유권을 이전하고 나면 다시는 거들떠 보면 안되는 것이죠.

그럼 good news는 뭘까요 ? 소유권 이전이 필요한 곳에 유용하게 쓸 수 있다는 것이죠.

1. 함수가 데이터 sink 처럼 행동하는 경우: 위에 void BigClassPrint(BigClassAutoPtr p)
  와 같은 예입니다. 값에 의한 전달 규칙에 따라 복사 생성자가 호출되면서 p 가 소유권을
  가져가게 됩니다. 그렇지만, BigClassPrint()는 일반 pointer를 통해 소유권을 전달
  받았을 때와는 달리 함수 안에서 delete p를 호출할 필요가 없습니다. 왜냐면,
  BigClassPrint()를 벗어날 때, p가 소멸되면서 p가 가리키는 객체도 소멸되기
  때문입니다. 예외가 발생하더라도 p는 소멸되므로 예외 안전성도 확보 되구요.
2. 함수가 데이터 source처럼 행동하는 경우
   BigClassAutoPtr f()
   {
      BigClassAutoPtr p(new BigClass());
      ......
      return p;
    } // 값에 의한 리턴이므로 복사 생성자가 호출되면서 소유권이 호출자에게 이전됨

이런 auto_ptr의 특이 체질 때문에 vector, list, set, map 과 같은 표준 컨테이너 클래스에는 auto_ptr이 쓰일 수가 없습니다. 왜냐면 표준 컨테이너 클래스의 element type으로 요구되는 사항이 복사 생성자와 복사 대입 연사자가 public 으로 선언되어 있고, 통상적인 동작을 한다는 것이기 때문입니다.(더 큰 문제는 컴파일은 되지만 실행시에 큰 문제가 생길 수가 있다는 것입니다) 지금은 이 이상 "왜?"에 대해서 설명할 수는 없지만... "이해가 안되면 그냥 외우시기" 바랍니다.

이런 auto_ptr의 문제점을 인식하고, C++ 표준화 위원회에서 const auto_ptr 에 대해서는 복사가 허용되지 않도록 바꾸긴 했지만, 여전히 맘이 편치 않은 구석이 있습니다. 그래서 그런지 저는 왠지 boost::shared_ptr<> 이나 boost::scoped_ptr<> 이 끌리더군요. auto_ptr과 사귄지 얼마나 됐다고, 벌써 딴 데 눈을 돌리냐구요 ? 그러게 말입니다. 아뭏든 이건 나중에 차차 알아보기로 하겠습니다.

auto_ptr을 쓰실 때 주의할 점 한 가지 더! auto_ptr은 배열 타입을 지원하지 않습니다. 즉,

std::auto_ptr<int> api(new int[10]);

이런식으로 하면 안 됩니다. 왜냐구요? auto_ptr은 객체를 삭제할 때, delete []로 하는 것이 아니라 delete로 하기 때문입니다. 음... 속으로 "delete [] 가 뭐지 ? 그냥 delete 랑 같은 것 아닌가 ?" 라고 생각하고 계신 분이 있군요. 그렇담. 다음에는 new/delete, new[]/delete [] 에 대해 좀 알아봐야 겠네요.

마지막으로 여러분께 당부하고 싶은 게 있습니다.

"auto_ptr은 복사하면 소유권이 이전된다"

는 걸 절대 잊지 마십시오.

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

소프트웨어는 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.07 03:31 [수정/삭제] [답글]

    객체지향에 관심이 많지만 시간도 없고 쓸 일은 더더욱 없어서 매일 호기심만입니다. 저희쪽에서는 오직 C만 쓰고, 모든 C컴파일러에서 잘 돌아가야 해서 compiler-specific한 기교도 거의 쓰질 못합니다. malloc/free도 절대 안쓰겠다는 사람들을 끈질기게 설득해서 이번 하는 프로젝트에 겨우 쓰게 되었는데, c++에 객체지향이라니... 꿈도 못 꿉니다. 점점 시대에 뒤쳐지는 것 같아요. 짜는 프로그램이 legacy하다보니 어쩔 수 없지만... 바로 몇 미터 안 떨어진 곳에 계시지만 그 동네는 auto_ptr 이 문제가 된다니 참 재미있다는 생각이 들었습니다.

    • 김윤수

      2007.03.07 05:39 신고 [수정/삭제]

      저희쪽 동네도 auto_ptr 이 문제가 되진 않습니다. 제가 공부하면서 정리해 본 것이니 너무 시대에 뒤떨어진다고 생각하실 필요는 없을 듯...
      요즘은 차라리 C++도 시대에 뒤떨어진 언어이고, JavaScript, Ruby, Python 이런 걸 해야 시대에 뒤떨어지지 않았다는 얘기 듣지 않을까요 ? 어찌됐든 아직도 C, C++ 가 쓰이는 곳은 많다는 것으로 위안을 삼으시면 될 듯합니다. 특히나 OS는 C++ 로 작성하는 경우도 정말 드물죠.

  2. 김규만

    2007.03.07 12:00 [수정/삭제] [답글]

    C++에서 포인터를 못쓰게하는 동료때문에 한번 써 봤어요.
    레퍼런스만로는 다형성 객체가 안되서리 디자인 패턴을 적용할 수 없더라구요.
    암튼 결론은 이것도 왠만하면 쓰지말라는 바람에 지금은 고생중..
    왜 쓰지 말라는거지? 그래도 포인터보단 똑똑하잖아??

  3. TrueKang

    2007.05.09 18:01 [수정/삭제] [답글]

    김규만님 스맛포인터마저 못쓰게 하면 전부 밸류나 객체 통째로 패스한다는건가요? 정말 안정적이긴 할거같은데...ㅎㅎ 패스할 객체 사이즈가 크다면....200ms나 us 단위로 수천수만개의 뎃타를 처리해야한다면....덜덜덜

    • 김규만

      2007.05.22 09:07 [수정/삭제]

      객체들은 레퍼런스로 패스하는 경우가 많아서 사이즈 걱정은 안해도 되요. 포인터를 쓰지 말라는 거죠. 다만 C++의 랭귀지 설계상 문제때문에 다형성을 구현하려면 포인터 밖에 안되요. 자바와 가장 큰 차이점이지요. 대부분 디자인 패턴들은 객체지향의 상속, 다형, 은닉등의 기능을 다 써야 하기 때문에, C++에선 포인터없이 구현이 안되는 경우가 많죠.

    • 김윤수

      2008.08.01 06:58 신고 [수정/삭제]

      김규만씨가 잘못 아셨나 봅니다. 레퍼런스로도 virtual 함수 호출이 가능합니다.

  4. wafe

    2007.11.22 08:36 신고 [수정/삭제] [답글]

    virtual 함수 호출의 측면에서 보자면 레퍼런스와 포인터의 차이가 없을텐데 레퍼런스로는 다형성을 사용할 수 없다는 게 무슨 말씀이신지 궁금하네요.

댓글을 남겨주세요.