2010년 10월 22일 금요일

간단한 템플릿 응용 #1 - 컨트롤 동적 생성

간단한 템플릿 응용 #1 - 컨트롤 동적 생성

 

사용자가 서브 클래싱한, 즉 메세지 핸들링을 추가한 컨트롤 컨트롤 동적으로 생성해야 하는 경우가 간혹 있습니다. 이런 경우 컨트롤의 윈도 객체는 힙 메모리상에 할당하게 되는데 컨트롤이 생성되어 사용되어 지고 있는 동안 서브 클래싱한 윈도 객체 또한 메모리 상에 유효한 상태로 존재하고 있어야 합니다.

 

이런 경우 컨트롤이 가장 최후에 보내는 메세지인 WM_NCDESTROY를 가로채는 메세지 핸들러를 서브클래싱한 윈도 객체의 메세지 맵에 추가하고 이 메세지 핸들러에서 자신을 메모리로부터 삭제하게 됩니다. MFC의 CWnd에서는 OnPostNcDestroy()라는 가상 함수를 이러한 용도로 제공하고 ATL/WTL에서는 OnFinalMessage()라는 가상 함수를 제공합니다.

 

예를 들어서 CMyButton 이라는 사용자 정의 컨트롤 클래스가 있다고 가정합니다.

 

class CMyButton : public CButton

{

  afx_msg void OnPaint();

  ...

  DECLARE_MESSAGE_MAP()

};

 

일반적으로 컨트롤 클래스들은 DDX 맵을 통해서 정적으로 서브 클래싱이 이루어진다고 가정하고 작성하죠. 이러한 컨트롤 클래스의 윈도 객체를 동적으로 힙 메모리에 생성하여 서브 클래싱하는 경우에는 앞서 언급한 객체의 생존 주기를 어떻게 관리하는가 하는 문제가 생깁니다.

 

BOOL CMyTestDlg::OnInitDialog()

{

  CDialog::OnInitDialog();

 

  CMyButton * pcButton = new CMyButton;

  pcButton->Create(...);

 

  // 누가 pcButton 을 삭제하나?

 

  return TRUE;

}

 

WM_NCDESTROY 메세지를 직접 처리할 필요가 없이 MFC 에서 제공하는 OnPostNcDestroy() 가상 함수를 다음과 같이 오버라이딩하도록 CMyButton 클래스를 수정합니다.

 

class CMyButton : public CButton

{

  virtual void PostNcDestroy()

  {

    CButton::PostNcDestroy();

    delete this;

  }

 

  afx_msg void OnPaint();

  ...

  DECLARE_MESSAGE_MAP()

};

 

CMyButton 클래스는 이제 자동 삭제 컨트롤 클래스입니다. 사용자는 힙 메모리에 생성한 CMyButton 객체가 컨트롤이 파괴되는 시점에서 동시에 메모리 상에서 해제가 된다는 것을 보장받게 됩니다. 하지만 대신에 한 가지 문제가 생겼습니다. 이제 CMyButton 컨트롤 클래스를 DDX 맵에서 사용을 하게되면 자동 변수를 삭제하는 에러가 발생하게 됩니다.

 

이 시점에서 C++ 개발자는 상속을 이용하려고 시도합니다.

 

 

class CMyButton : public CButton

{

  afx_msg void OnPaint();

  ...

  DECLARE_MESSAGE_MAP()

};

 

class CMyButtonDyn : public CMyButton

{

  virtual void PostNcDestroy()

  {

    CButton::PostNcDestroy();

    delete this;

  }

 

};

 

BOOL CMyTestDlg::OnInitDialog()

{

  CDialog::OnInitDialog();

 

  CMyButtonDyn * pcButton = new CMyButton;

  pcButton->Create(...);

 

  return TRUE;

}

 

위와 같은 과정은 매우 전형적인 시나리오입니다. 매번 프로그래머는 똑같은 작업을 반복하고 있습니다. 이와 같이 똑같은 작업이 반복되어질 때 우리는 템플릿의 사용을 고려할 수 있습니다.

 

template<typename TWnd>

class AutoDeleteHook : public TWnd

{

private:

  typedef TWnd BaseClass;

 

  // Overrides

protected:

  // MFC's CWnd::PostNcDestroy()

  virtual void PostNcDestroy()

  {

    BaseClass::PostNcDestroy();

    delete this;

  }

};

 

 

class CMyButton : public CButton

{

  afx_msg void OnPaint();

  ...

  DECLARE_MESSAGE_MAP()

};

 

 

BOOL CMyTestDlg::OnInitDialog()

{

  CDialog::OnInitDialog();

 

  CMyButtonn * pcButton = new AutoDeleteHook<CMyButton>;

  pcButton->Create(...);

 

  return TRUE;

}

 

이제 매번 추가적으로 프로그래머가 수작업으로 클래스를 새로 작성하는 대신에 간단한 템플릿 클래스를 사용함으로써 컴파일러가 일률적이면서 반복적이지만 타입 세이프한 코드를 자동으로 작성해주게 만듭니다.

 

ATL/WTL이라면 다음과 같은 템플릿 클래스를 사용할 수 있습니다.

 

template<typename TWindowImpl>

class AutoDeleteHook : public TWindowImpl

{

private:

  typedef TWindowImpl BaseClass;

 

  // Overrides

protected:

  // ATL::CWindowImplBaseT<TBase, TWinTraits>::OnFinalMessage()

  virtual void OnFinalMessage(HWND hWnd)

  {

    BaseClass::OnFinalMessage( hWnd );

    delete this;

  }

};

 

여기서 잠깐 위의 예제를 다시 살펴봅니다.

 

BOOL CMyTestDlg::OnInitDialog()

{

  CDialog::OnInitDialog();

 

  CMyButtonDyn * pcButton = new CMyButton;

  pcButton->Create(...);

 

  return TRUE;

}

 

위의 코드에는 메모리 누출의 가능성이 있습니다.

 

pcButton->Create(...); 에서 컨트롤의 생성이 실패하는 경우에 서브 클래싱도 일어나지 않고 따라서 OnNcPostDestory()는 호출되지 않습니다. 따라서 동적으로 생성된 pcButton은 삭제되지 않고 메모리 누출이 생깁니다. 따라서 다음과 같은 코드를 추가해주는 것이 안전합니다.

 

BOOL CMyTestDlg::OnInitDialog()

{

  CDialog::OnInitDialog();

 

  CMyButtonDyn * pcButton = new CMyButton;

  pcButton->Create(...);

 

  if( !::IsWindow( *pcButton ) )

  {

    delete pcButton;

  }

 

  return TRUE;

}

 

위에서 작성한 템플릿 클래스에는 아직도 한 가지 문제가 남아 있습니다.

 

 

class CMyColorButton : public CButton

{

  CMyColorButton(COLORREF clrText, COLORREF clrBk);

  afx_msg void OnPaint();

  ...

  DECLARE_MESSAGE_MAP()

};

 

새로 작성한 CMyColorButton 컨트롤 클래스는 위에서 작성한 AutoDeleteHook<TWnd> 템플릿 클래스를 사용할 수 없습니다. CMyColorButton의 생성자가 입력 인자를 받기 때문입니다. 위와 같이 템플릿 클래스의 템플릿 파라미터가 상속 관계에 관여하게 되는 경우에는 위와 유사한 생성자 문제가 항상 생기기 마련입니다. C++ 템플릿 클래스의 멤버 변수는 모두 구현(Instantiated) 되지 않고 실재로 사용되는 멤버함수만 구현됩니다. 이러한 사실을 응용하여 다음과 같이 생성자가 입력 인자를 5개 까지 받는 경우를 고려할 수 있게 템플릿 클래스를 확장해 봅니다.

 

template<typename TWnd>

class AutoDeleteHook : public TWnd

{

private:

  typedef TWnd BaseClass;

 

  // Overrides

protected:

  // MFC's CWnd::PostNcDestroy()

  virtual void PostNcDestroy()

  {

    BaseClass::PostNcDestroy();

    delete this;

  }

 

  // Constructors

public:

  AutoDeleteHook() : BaseClass() { }

 

  template<typename P1>

  AutoDeleteHook(P1 p1) : BaseClass( p1 ) { }

 

  template<typename P1, typename P2>

  AutoDeleteHook(P1 p1, P2 p2) : BaseClass( p1, p2 ) { }

 

  template<typename P1, typename P2, typename P3>

  AutoDeleteHook(P1 p1, P2 p2, P3 p3) : BaseClass( p1, p2, p3 ) { }

 

  template<typename P1, typename P2, typename P3, typename P4>

  AutoDeleteHook(P1 p1, P2 p2, P3 p3, P4 p4) : BaseClass( p1, p2, p3, p4 ) { }

 

  template<typename P1, typename P2, typename P3, typename P4, typename P5>

  AutoDeleteHook(P1 p1, P2 p2, P3 p3, P4 p4, P5 p5) : BaseClass( p1, p2, p3, p4, p5 ) { }

 

private:

  AutoDeleteHook(AutoDeleteHook const &);

  AutoDeleteHook & operator =(AutoDeleteHook const &);

 

  // Destructor

public:

  virtual ~AutoDeleteHook()

  {

    ASSERT( !::IsWindow( *this ) );

  }

};  // AutoDeleteHook<TWnd>

 

 

BOOL CMyTestDlg::OnInitDialog()

{

  CDialog::OnInitDialog();

 

  CMyButtonDyn * pcButton = new AutoDeleteHook<CMyButton>;

  pcButton->Create(...);

 

  if( !::IsWindow( *pcButton ) delete pcButton;

 

  CMyColorButton * pcColorBtn = new AutoDeleteHook<CMyColorButton>( RGB(255, 255, 0), RGB(0, 255, 255) );

  pcColorBtn->Create(...);

 

  if( !::IsWindow( *pcColorBtn ) delete pcColorBtn;

  ...

 

  return TRUE;

}

댓글 없음:

댓글 쓰기