auto_ptr의 두 얼굴
저번 글을 통해서 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타입에 관하여)
'C & C++ 관련' 카테고리의 다른 글
스마트 포인터 (0) | 2009.07.26 |
---|---|
auto_ptr 템플릿 클래스 소개 (0) | 2009.07.26 |
SmartPointer (0) | 2009.07.26 |