간단한 템플릿 응용 #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;
}
댓글 없음:
댓글 쓰기