I have an MFC project with a modal dialog. At the begin of my code, I have a try
/catch
statement and I try to throw
exceptions in various place of my code.
The exception from OnBnClickedButton1
or OnTvnGetdispinfoTree
is handled properly, but the throw
from OnTvnSelchangedTree
is unhandled - see picture below.
It is a standard dialog-based MFC project generated by the Visual Studio 2022 wizard. On the dialog are a button and CTreeControl
.
Main code with try
/catch
statement:
try {
CMFCExceptDlg dlg;
m_pMainWnd = &dlg;
INT_PTR nResponse = dlg.DoModal();
if (nResponse == IDOK) {
TRACE(traceAppMsg, 0, "OK...\n");
}
else if (nResponse == IDCANCEL) {
TRACE(traceAppMsg, 0, "Cancel...\n");
}
else if (nResponse == -1) {
TRACE(traceAppMsg, 0, "Warning: dialog creation failed, so application is
terminating unexpectedly.\n");
TRACE(traceAppMsg, 0, "Warning: if you are using MFC controls on the dialog, you
cannot #define _AFX_NO_MFC_CONTROLS_IN_DIALOGS.\n");
}
}
catch(std::exception &exc) {
const char *x = exc.what();
TRACE(traceAppMsg, 0, exc.what());
}
Dialog code (.h file):
class CMFCExceptDlg : public CDialogEx
{
public:
CMFCExceptDlg(CWnd* pParent = nullptr); // standard constructor
#ifdef AFX_DESIGN_TIME
enum { IDD = IDD_MFC_EXCEPT_DIALOG };
#endif
protected:
virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV support
protected:
HICON m_hIcon;
CTreeCtrl m_cTree;
virtual BOOL OnInitDialog();
DECLARE_MESSAGE_MAP()
public:
afx_msg void OnTvnGetdispinfoTree(NMHDR *pNMHDR, LRESULT *pResult);
afx_msg void OnBnClickedButton1();
afx_msg void OnTvnSelchangedTree(NMHDR* pNMHDR, LRESULT* pResult);
};
Implementation of dialog (.cpp):
CMFCExceptDlg::CMFCExceptDlg(CWnd* pParent /*=nullptr*/)
: CDialogEx(IDD_MFC_EXCEPT_DIALOG, pParent)
{
m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
}
void CMFCExceptDlg::DoDataExchange(CDataExchange* pDX)
{
CDialogEx::DoDataExchange(pDX);
DDX_Control(pDX, IDC_TREE, m_cTree);
}
BEGIN_MESSAGE_MAP(CMFCExceptDlg, CDialogEx)
ON_NOTIFY(TVN_GETDISPINFO, IDC_TREE, &CMFCExceptDlg::OnTvnGetdispinfoTree)
ON_BN_CLICKED(IDC_BUTTON1, &CMFCExceptDlg::OnBnClickedButton1)
ON_NOTIFY(TVN_SELCHANGED, IDC_TREE, &CMFCExceptDlg::OnTvnSelchangedTree)
END_MESSAGE_MAP()
// CMFCExceptDlg message handlers
BOOL CMFCExceptDlg::OnInitDialog()
{
CDialogEx::OnInitDialog();
m_cTree.InsertItem(LPSTR_TEXTCALLBACK, TVI_ROOT);
m_cTree.InsertItem(LPSTR_TEXTCALLBACK, TVI_ROOT);
m_cTree.InsertItem(LPSTR_TEXTCALLBACK, TVI_ROOT);
m_cTree.InsertItem(LPSTR_TEXTCALLBACK, TVI_ROOT);
return TRUE;
}
void CMFCExceptDlg::OnTvnGetdispinfoTree(NMHDR *pNMHDR, LRESULT *pResult)
{
LPNMTVDISPINFO pTVDispInfo = reinterpret_cast<LPNMTVDISPINFO>(pNMHDR);
*pResult = 0;
throw std::exception("GetDispInfo");
}
void CMFCExceptDlg::OnBnClickedButton1()
{
throw std::exception("In Click Button Error");
}
void CMFCExceptDlg::OnTvnSelchangedTree(NMHDR* pNMHDR, LRESULT* pResult)
{
LPNMTREEVIEW pNMTreeView = reinterpret_cast<LPNMTREEVIEW>(pNMHDR);
// TODO: Add your control notification handler code here
*pResult = 0;
throw std::exception("OnTvnSelchangedTree");
}
The throw
from this last handler (SelChanged
) doesn't go to the catch
statement, but finishes by an error instead:
You cannot throw (C++) exceptions across foreign stack frames1. The TVN_SELCHANGED
notification is sent by the tree-view control to its parent, allowing it to respond to changes in UI state. When the callback returns, control crosses back into the control implementation.
The control isn't prepared for C++ exceptions. For all we know, it could be implemented in C and doesn't even know what C++ exceptions are. It is thus crucial that C++ exceptions never escape your window procedure implementation.
The easiest way to ensure this doesn't happen2 is by applying the noexcept
specifier to the window procedure. Since this is MFC you don't control the window procedure and instead have to mark all message handlers as noexcept
. With that in place, any attempt to throw a C++ exception across a message handler will have the language runtime initiate a controlled emergency shutdown.
Out of all outcomes, this is the best you can hope for.
If you need to collect information when this happens, you can have your installer set up the system to collect user-mode dumps, allowing you to perform post-mortem analysis of what could well be a bug.
A note on TVN_SELCHANGED
specifically: This is a notification meant for observation only. If the parent decides to handle this notification, the handler's return value is expressly ignored. It seems rather unusual to want to scream "Oh noes!" when no one is listening anyway.
If you wish to cancel selection, you need to respond to the TVN_SELCHANGING
message instead. Its return value is observed and controls whether the selection change is allowed.
1 Details in Raymond Chen's blog post When you transfer control across stack frames, all the frames in between need to be in on the joke.
2 An alternative would be a function-try-block. This is more involved as you have to ensure you aren't "swallowing" exceptions you didn't expect.