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

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

by esstory 2008.10.08


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


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

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

 

지난번 포스팅에서 간단하게 포인터를 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++]소멸자에서 가상함수 호출하기  (8) 2008.10.08
[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

댓글8

  • BlogIcon jindog 2008.10.08 11:30

    Effective C++에서 강조하는 내용이 검색되었나보네요 ^^

    // 저 같은 초보자는 더 좋은 방법은 모르고 ... ;;; 읽기만 해서 죄송 ;;;
    답글

    • BlogIcon esstory 2008.10.08 11:43 신고

      Effective C++ 3rd Edition 에 나온거 같더라구요. 저는 2nd 만 가지고 있어서.. 거의 같은 내용일거라 3편은 구입안했는데 이 참에 사야 할까 봐요
      1등 댓글 감사합니다 ㅎㅎ

  • BlogIcon 조순현 2008.10.08 22:39

    1. 위의 경우엔 소멸된 상속 객체의 함수를 호출하지만, 소멸된 객체의 어떤 멤버 변수나 가상 함수 테이블도 사용하지 않으므로 문제가 없네요. 하지만 일반적인 상황엔 위험할 것 같습니다.

    2. virtual이 문제인 이유도 1번과 연관이 있습니다. virtual을 사용하면 실제로 연결된 함수를 상속 객체의 가상 함수 테이블에서 찾아야 하는데, 그 가상 함수 테이블을 가리키는 포인터 영역이 사라진 상태니 문제가 되겠네요.
    답글

    • BlogIcon esstory 2008.10.08 22:32 신고

      속시원한 답변이시네요 ^^;
      그럴거 같다고 생각은 했는데 순현님 답변 들으니 수긍이 갑니다.
      좋은 의견 남겨주셔서 감사합니다 ~*

    • BlogIcon 조순현 2008.10.08 23:30

      에구 죄송합니다;

      방금 다시 조사해 봤는데, 2번은 제 설명이 잘못됐네요. 상속 클래스의 소멸자를 호출하고 나면, 가상 함수 테이블 포인터가 기반 클래스의 가상 함수 테이블을 가리키게 바뀌네요.

      기반 클래스의 가상 함수 테이블을 가리키고 있고 따라서 그 구조에 맞게 역참조해야 하는 포인터를 이용해서, 상속 클래스의 가상 함수 테이블 구조에 맞춰 역참조하려고 하니 문제가 되는 것이었네요. 즉, IMyAutoPtrBase 클래스의 가상 함수 테이블을 가리키는 포인터를 이용해서 Derived 클래스의 가상 함수 테이블에 있는 정보를 얻으려고 하니 안 되는 것이었습니다. 비유하자면, 애꿎은 전화번호부를 꺼내놓고 오늘 날씨 정보를 찾는 상황입니다.

    • BlogIcon esstory 2008.10.09 07:51 신고

      아 머리가 아파옵니다 ㅎㅎ
      Derived 클래스의 가상 함수 테이블이 이미 없어진 상황이라 문제가 생긴거라고 대충 이해했습니다
      좀더 공부하고 와야겠어요.
      늦은 밤까지 이렇게 확인까지 해 주시고 정말 감사합니다.

  • BlogIcon 맑은독백 2008.10.09 15:53 신고

    "소멸자를 virtual 로 선언하지 않으면 상속받은 클래스의 소멸자가 호출되지 않는다."

    저도 한 클래스에 이걸 빼먹어 메모리 누수가 생기더라구요 ㅋ
    valgrind 이용해서 전 잡았는데.. 뭐든 기본이 참 중요한거 같습니다.
    답글

    • BlogIcon esstory 2008.10.09 16:03 신고

      뭐든지 습관처럼 되어 버리면 실수하지 않는 법인데. C++ 에는 습관으로 익혀야 하는 기술이 너무 많아 실수로 문제가 생길 가능성이 너무 큰거 같습니다. 아슬아슬하다고 해야 할까요
      그래도 여태 이걸로 해 왔고 앞으로도 몇년은 더 버틸거 같습니다.
      맑은 독백님 1일 2댓글은 영광입니다 ㅎㅎ