Deleting the C++ object associated with an ATL dialog box
by calling "delete this" in the WM_NCDESTROY handler or OnFinalMessage()
results in an assert in Atlwin.h, line 2281.
The object being referred to by "pThis" has already been deleted.
Consider ATL's default dialog box procedure:
template <class TBase>
LRESULT CALLBACK CDialogImplBaseT< TBase >::DialogProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
CDialogImplBaseT< TBase >* pThis = (CDialogImplBaseT< TBase >*)hWnd;
// set a ptr to this message and save the old value
MSG msg = { pThis->m_hWnd, uMsg, wParam, lParam, 0, { 0, 0 } };
const MSG* pOldMsg = pThis->m_pCurrentMsg;
pThis->m_pCurrentMsg = &msg;
// pass to the message map to process
LRESULT lRes;
BOOL bRet = pThis->ProcessWindowMessage(pThis->m_hWnd, uMsg, wParam, lParam, lRes, 0);
// restore saved value for the current message
ATLASSERT(pThis->m_pCurrentMsg == &msg);
pThis->m_pCurrentMsg = pOldMsg;
// set result if message was handled
if(bRet)
{
switch (uMsg)
{
case WM_COMPAREITEM:
case WM_VKEYTOITEM:
case WM_CHARTOITEM:
case WM_INITDIALOG:
case WM_QUERYDRAGICON:
case WM_CTLCOLORMSGBOX:
case WM_CTLCOLOREDIT:
case WM_CTLCOLORLISTBOX:
case WM_CTLCOLORBTN:
case WM_CTLCOLORDLG:
case WM_CTLCOLORSCROLLBAR:
case WM_CTLCOLORSTATIC:
return lRes;
break;
}
::SetWindowLong(pThis->m_hWnd, DWL_MSGRESULT, lRes);
return TRUE;
}
if(uMsg == WM_NCDESTROY)
{
// clear out window handle
HWND hWnd = pThis->m_hWnd;
pThis->m_hWnd = NULL;
// clean up after dialog is destroyed
pThis->OnFinalMessage(hWnd);
}
return FALSE;
}
Typically, you will be calling DestroyWindow()/EndDialog() in a
WM_CLOSE or a WM_COMMAND handler. Here's the sequence of events when you close
a dialog box:
DialogProc() is called with WM_CLOSE.
ProcessWindowMessage() calls your WM_CLOSE
handler.
In your WM_CLOSE handler, you call
DestroyWindow().
This ends up calling DialogProc again with
WM_NCDESTROY.
ProcessWindowMessage() calls your WM_NCDESTROY
handler.
You call "delete this" in your WM_NCDESTROY
handler.
RESULTS: When you come back from ProcessWindowMessage(), because the C++
class has been deleted, pThis no longer points to a valid object, and therefore
the assert is returned.
The following is another possible scenario
(note that the first four steps are the same as above):
DialogProc has a case for WM_NCDESTROY and calls
OnFinalMessage().
You call "delete this" in OnFinalMessage().
The stack unwinds to the original DialogProc call (with a
WM_CLOSE).
RESULTS: Same problem. After the call to ProcessWindowMessage, try to use
pThis, but it no longer points to valid memory.
Notify DialogProc when the dialog box class has been
deleted; this can be accomplished by adding a member variable called
m_bAutoDelete to the dialog box class. Setting this to TRUE causes the dialog
box class to delete itself when the window is destroyed. Use the following
code:
// Constant value used to determine if we should delete ourselves later.
#define DEFERDELETE 2
class CMyDlg : public CAxDialogImpl<CMyDlg>
{
public:
// Variable that tells us if we want to auto-delete ourselves.
BYTE m_bAutoDelete;
// Set m_bAutoDelete to TRUE to automatically delete ourselves.
CMyDlg() : m_bAutoDelete (TRUE)
{
}
// Override GetDialogProc to provide our own DialogProc.
WNDPROC GetDialogProc()
{
return MyDialogProc;
}
// Our own dialog procedure that is mostly copied from
// CDialogImplBaseT<>::DialogProc() in Atlwin.h.
static LRESULT CALLBACK MyDialogProc(HWND hWnd, UINT uMsg,
WPARAM wParam, LPARAM lParam)
{
CMyDlg* pThis = (CMyDlg*)hWnd;
// Set a ptr to this message and save the old value.
MSG msg = { pThis->m_hWnd, uMsg, wParam, lParam, 0, { 0, 0 } };
const MSG* pOldMsg = pThis->m_pCurrentMsg;
pThis->m_pCurrentMsg = &msg;
// Pass to the message map to process.
LRESULT lRes;
BOOL bRet = pThis->ProcessWindowMessage(pThis->m_hWnd, uMsg, wParam,
lParam, lRes, 0);
// If window has been destroyed and this is the last message,
// then delete ourselves.
if (DEFERDELETE == pThis->m_bAutoDelete && pOldMsg == NULL)
{
delete pThis;
return FALSE;
}
// Restore saved value for the current message.
ATLASSERT(pThis->m_pCurrentMsg == &msg);
pThis->m_pCurrentMsg = pOldMsg;
// Set result if message was handled.
if(bRet)
{
switch (uMsg)
{
case WM_COMPAREITEM:
case WM_VKEYTOITEM:
case WM_CHARTOITEM:
case WM_INITDIALOG:
case WM_QUERYDRAGICON:
case WM_CTLCOLORMSGBOX:
case WM_CTLCOLOREDIT:
case WM_CTLCOLORLISTBOX:
case WM_CTLCOLORBTN:
case WM_CTLCOLORDLG:
case WM_CTLCOLORSCROLLBAR:
case WM_CTLCOLORSTATIC:
return lRes;
break;
}
::SetWindowLong(pThis->m_hWnd, DWL_MSGRESULT, lRes);
return TRUE;
}
if(uMsg == WM_NCDESTROY)
{
// Clear out window handle.
HWND hWnd = pThis->m_hWnd;
pThis->m_hWnd = NULL;
// Clean up after dialog box is destroyed.
pThis->OnFinalMessage(hWnd);
// If we want to automatically delete ourselves...
if (pThis->m_bAutoDelete)
{
// If no outstanding messages to process in call stack,
// m_pCurrentMsg will be NULL so we can delete ourselves.
if (pThis->m_pCurrentMsg == NULL)
delete pThis;
// Else set a flag so we can delete ourselves later.
else
pThis->m_bAutoDelete = DEFERDELETE;
}
}
return FALSE;
}
...
};
In CDialogImplBaseT<>::DialogProc(), m_pCurrentMsg is
set so you can call GetCurrentMessage() to retrieve the current message from
any method in your dialog class. This problem and solution applies to any
CWindowImplRoot-derived class.