본문 바로가기
개발/C C++

[C++]소멸자에서 가상함수 호출하기

by esstory 2008. 10. 8.


소멸자에서 가상함수 호출하기 시도


요즘은 거의 코딩할일이 없다보니 블로그에 개발 관련 글을 포스팅할 기회조차 없네요

개발자라는 타이틀은 이제 빼야 할 때가 왔나 봅니다.  ㅎㅎ

 

지난번 포스팅에서 간단하게 포인터를 Wrapping 하는 템플릿을 소개한적이 있습니다.

[C++]포인터 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

 

 

다행히 기능은 구현했으나 몇 가지 의문이 생겼습니다.

  • 이 경우에도 상속 클래스의 소멸자 호출 > 베이스 클래스의 소멸자가 호출됩니다. 그렇다면 베이스 클래스에서 이미 소멸된 상속 클래스의 함수를 호출하는데 문제는 혹시 없을까요. 위 코드는 정상적으로 수행되긴 하지만 다른 문제가 없는가 의문이 생깁니다.

  • 상속받은 클래스 IMyAutoDerived2::Clear() 함수를 virtual 로 선언하면 런타임에서 오류가 발생합니다. 이 상황에서 virtual 이 왜 문제인지 모르겠더군요.

 

의문은 뒤로 하고 이번 포스팅은 마칩니다. 혹시 더 나은 해결책이나, 제 의문을 해결 해 주실 고수분들 환영합니다.

 

'개발 > 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

댓글