소멸자에서 가상함수 호출하기 시도
요즘은 거의 코딩할일이 없다보니 블로그에 개발 관련 글을 포스팅할 기회조차 없네요
개발자라는 타이틀은 이제 빼야 할 때가 왔나 봅니다. ㅎㅎ
지난번 포스팅에서 간단하게 포인터를 Wrapping 하는 템플릿을 소개한적이 있습니다.
나름 쓸모 있어서 요긴하게 사용하고 있는데요.
사용을 하다 보니 이 템플릿의 변종들이 필요하게 되었습니다.
지난번에 만든 포인트를 감싸는 템플릿 코드는 아래와 같습니다.
template <class T>
class IMyAutoPtr
{
public:
IMyAutoPtr(){m_pPtr = NULL;}
~IMyAutoPtr()
{
if (m_pPtr)
m_pPtr->Release();
m_pPtr = NULL;
}
T* m_pPtr;
// 연산자오버로딩- IXXControlA 을요청하는경우IXXControlA 로자연스럽게변환
operator T*() const {return m_pPtr;}
// 포인터(m_pPtr) 자체에값을할당할경우에는T** 파라미터가필요하다.
T** operator&() throw()
{
ATLASSERT(m_pPtr==NULL);
return &m_pPtr;
}
bool operator!() const throw()
{
return (m_pPtr == NULL);
}
T& operator*() const
{
ASSERT(m_pPtr);
return *m_pPtr;
}
T* operator =(T* data) {m_pPtr = data; return m_pPtr;} // = 연산자오버로딩
bool operator==( T* pT) const throw()
{
return m_pPtr == pT;
}
T* operator->() {return m_pPtr;} // -> 연산자오버로딩
};
위 템플릿으로 아래와 같은 코드 대신
CMyClass* pMyClass = new CMyClass;
pMyClass->DoSomething();
// 모두사용후
pMyClass->Release();
포인터 소멸에 대한 처리는 잊을 수가 있게 되었습니다.
IMyAutoPtr<CMyClass> MyClassPtr;
MyClassPtr = new CMyClass;
pMyClass->DoSomething();
// 모두사용후처리할일없음
문제는 Wrapping 하는 클래스마다 소멸하는 방법이 조금씩 차이가 있어서
pMyClass->Release();
또는
pMyClass->DestroyWindow()
delete pMyClass;
또는
delete pMyClass;
식으로 다양한 변종 클래스들이 필요하게 되었습니다.
그래서 IMyAutoPtr<> 를조금수정해서 IMyAutoPtrBase<> 로 만들기로마음먹었습니다. IMyAutoPtrBase<> 템플릿은 아래와 같이 작성했습니다.
template <class T>
class IMyAutoPtrBase
{
public:
IMyAutoPtrBase(){m_pPtr = NULL;}
virtual ~IMyAutoPtrBase()
{
Clear();
}
virtual void Clear()
{
// Do Something
TRACE("IMyAutoPtrBase::Clear \n");
}
// 중략..
제 짧은 생각에 소멸자에서 가상함수 Clear()를 호출하고, IMyAutoPtrBase<> 를 상속받은 템플릿에서 각자 자신만의 Clear() 함수를 만들면 되지 않을까 하는 생각이었습니다. 그래서 아래와 같은 2가지 IMyAutoPtrBase<> 를 상속받는 템플릿을 만들게 되었습니다.
template <class T>
class IMyAutoDerived1 : public IMyAutoPtrBase<T>
{
public:
void Clear()
{
TRACE("IMyAutoDerived1::Clear \n");
delete m_pPtr;
}
T* operator =(T* data) {m_pPtr = data; return m_pPtr;} // = 연산자오버로딩
};
template <class T>
class IMyAutoDerived2 : public IMyAutoPtrBase<T>
{
public:
void Clear()
{
TRACE("IMyAutoDerived2::Clear \n");
m_pPtr->Release();
}
T* operator =(T* data) {m_pPtr = data; return m_pPtr;} // = 연산자오버로딩
};
상속받은 클래스들은 = 연산자와 Clear() 함수만 직접 구현하는 걸로 모든 것이 순조로운 줄 알았습니다.
소멸자에서 가상함수를 호출해서는 안된다.!!!
하지만 언제나 그렇듯, 생각처럼 쉽게 되는 건 없더군요.
Base 클래스의 소멸자에서 호출한 Clear() 함수는 제가 의도한 대로 상속받은 클래스의 Clear() 함수를 호출하는게 아니라 자기 자신의 Clear() 를 호출하더군요.
덕분에 이 템플릿 고유의 기능인 자원을 알아서 반환해야 하는 기능자체가 무용지물이 되고 말았습니다.
이유를 확인하기 위해 좀 더 위해하기 쉬운 Dummy 베이스 클래스와 상속 클래스를 만들어봤습니다.
테스트를 위해 만든 클래스입니다.
class CBase
{
public:
CBase()
{
TRACE("CBase::CBase\n");
}
// 소멸자를 virtual 로 선언하지 않으면 상속받은 클래스의 소멸자가 호출되지 않는다.
virtual ~CBase()
{
TRACE("CBase::~CBase\n");
Clear();
}
virtual void Clear()
{
TRACE("CBase::Clear\n");
}
};
class CDerived : public CBase
{
public:
CDerived()
{
TRACE("CDerived::CDerived\n");
}
~CDerived()
{
TRACE("CDerived::~CDerived\n");
}
void Clear()
{
TRACE("CDerived::Clear\n");
}
};
이렇게 만든 클래스를 테스트하기 위한 테스트 코드입니다.
CBase* pBase = new CDerived;
delete pBase;
생성자, 소멸자, Clear() 에서 호출한 TRACE 는 다음과 같이 찍혔습니다.
CBase::CBase
CDerived::CDerived
CDerived::~CDerived
CBase::~CBase
CBase::Clear
C++ 에서 새로운 클래스의 생성은 Base 클래스의 생성자 > 상속받은 클래스의 생성자 순으로 호출됨을 알 수 있습니다.
그리고 소멸자는 상속받은 클래스의 소멸자 > 베이스 클래스의 소멸자가 호출되는 순서였습니다.
그래서, 베이스 클래스의 소멸자가 호출되는 시점에는 이미 상속받은 클래스의 소멸자가 호출된 이후였기 때문에 소멸자에서 호출한 가상함수는 자기 자신의 Clear() 함수만 호출하고 만 것으로 보입니다.
생각에 여기에 미치고 난 후에야 “소멸자에서 가상함수 호출” 로 인터넷을 검색해 봤습니다.
그랬더니 이미 많은 블로거들이 이에 대한 글들을 적으셨더군요. (솔직히 C++ 관련 깊이 있는 내용이 이렇게 많이 검색되는 걸 보고 많이 놀랐습니다. 아직도 C++ 로 개발하시는 개발자 분들 많으신가 봐요. 자바에 다 파 묻히신 줄 알았는데 반갑더라구요)
클래스의 생성자나 소멸자에서 그 클래스의 가상 함수를 호출하지 말 것
저는 처음 안 사실인데, 뒷북이라 좀 쑥스러웠습니다. 여러분들은 이미 알고 계신 문제였죠?
소멸자에서 가상함수를 호출하면 안 된다는 좋은 교훈은 얻었지만, 아직 제가 찾는 해답은 얻지 못했습니다.
소멸자에서 가상함수 호출 문제를 우회해서 해결 방안을 찾기
몇 번의 시행 착오 끝에 찾은 방법은 IMyAutoPtrBase<> 를 아래와 같이 수정했습니다.
template <class T, class Derived>
class IMyAutoPtrBase
{
public:
IMyAutoPtrBase(){m_pPtr = NULL;}
virtual ~IMyAutoPtrBase()
{
Derived* pT = static_cast<Derived*>(this);
pT->Clear();
}
// Clear() 함수를베이스클래스에서는제거합니다.
//virtual void Clear()
//{
// // Do Something
// TRACE("IMyAutoPtrBase::Clear \n");
//}
T* m_pPtr;
// 중략
그리고 상속받은 클래스에서는 Clear() 를 구현해 주면 됩니다.
template <class T>
class IMyAutoDerived1 : public IMyAutoPtrBase<T, IMyAutoDerived1<T> >
{
public:
void Clear()
{
TRACE("IMyAutoDerived1::Clear \n");
delete m_pPtr;
}
T* operator =(T* data) {m_pPtr = data; return m_pPtr;} // = 연산자오버로딩
};
template <class T>
class IMyAutoDerived2 : public IMyAutoPtrBase<T, IMyAutoDerived2<T> >
{
public:
void Clear()
{
TRACE("IMyAutoDerived2::Clear \n");
m_pPtr->Release();
}
T* operator =(T* data) {m_pPtr = data; return m_pPtr;}
};
베이스의 소멸자에서 사용한
Derived* pT = static_cast<Derived*>(this);
pT->Clear();
코드는 템플릿을 사용하면서 알게된 정말 유용한 코드입니다. (ATL 소스를 따라 가다 발견했습니다)
베이스는 자신을 상속받을 클래스의 타입정보를 템플릿 파라미터로 받아 들여 자신을 상속받은 클래스의 this 로 타입 전환한 다음 상속받은 클래스의 함수를 호출하는 방식입니다.
상속된 클래스가 베이스 클래스의 함수를 호출하는 것이 일반적이지만 위 방법은 반대의 경우도 손쉽게 호출 할 수 있도록 하고, 상속된 클래스들의 Clear() 함수를 강제 구현하도록 할 수도 있습니다.(구현하지 않으면 컴파일 오류나니까요)
테스트를 위해 아래와 같은 코드를 준비했습니다.
IMyAutoDerived1<CBase> pDer1;
pDer1 = (CBase*)(new CBase);
IMyAutoDerived2<CDerived> pDer2;
pDer2 = new CDerived;
수행결과는 아래 처럼 제가 원하는 대로 IMyAutoPtrBase<> 의 소멸자에서 호출한 Clear() 함수가 정상 수행 되었습니다.
IMyAutoDerived2::Clear
IMyAutoDerived1::Clear
다행히 기능은 구현했으나 몇 가지 의문이 생겼습니다.
의문은 뒤로 하고 이번 포스팅은 마칩니다. 혹시 더 나은 해결책이나, 제 의문을 해결 해 주실 고수분들 환영합니다.
'개발 > C C++' 카테고리의 다른 글
[C/C++]자주 하는 실수 (15) | 2008.10.22 |
---|---|
[C++]포인터 Wrapping 클래스 만들기 (4) | 2008.09.09 |
[C/C++] enum, 보다 나은 enum (19) | 2008.07.21 |
싱글톤 클래스 (5) | 2008.07.01 |
[C++]STL Container 조합하기 (5) | 2007.11.12 |
[C/C++]유용한 #pragma directive (9) | 2007.09.05 |
댓글