Introduction
Assertions are a very effective debugging tool for C/C++ code. But, a very subtle problem exists with assertions that can cause you to waste a lot of debugging time chasing the wrong problem.
Background
There are two main ways to assert in Windows applications: The assert()
macro (defined in assert.h) and the _ASSERT
macros (defined in crtdbg.h). The main difference is that the assert()
macro calls the _assert()
API and _ASSERT()
calls the _CrtDbgReport()
API.
Most Windows developers use the _ASSERT
macros. They offer many more features, flexibility, and offer some protection against the recursion problem described below. Take a look at all the options available in crtdbg.h. Unless you are forced to use assert()
for cross-platform or some other valid reason, you should use the _ASSERT()
macros.
The Problem With Assertions -- Unintentional Recursion
Assertions usually put up a dialog box, which is created with the Windows API MessageBox()
. MessageBox()
has its own Windows message pump that runs while the dialog is displayed. So the thread that has asserted really hasn’t stopped at all. It is still running, processing Windows messages, and, most importantly, calling your application’s message handlers. It makes no difference if the MessageBox is application, system or task modal.
The problem comes in when you add assertions to your message handlers. The first assertion can cause a second assertion, completely hiding the first assertion.
This exact scenario occurred at my workplace several times to several different developers/testers. The developers were being very diligent by putting assertions into the WM_PAINT
handler to catch any attempt to draw before our large application had been fully initialized (DB opened, files opened, objects created, etc.). As with most assertions, we never expected these to be hit. They were only there to catch major programming flaws like someone trying to draw before initializing the application. Normally, if there was a failure during startup, it would have been reported to the user via our regular error handling.
When these assertions were hit, we were very perplexed at how it was possible to get to the WM_PAINT
handler before major portions of the application were initialized -- without any errors being reported by the initialization code.
It was only until I scrolled many levels up the stack before I noticed that we were actually inside a 2nd assertion and that the real problem (the first assertion) had been totally obscured.
Here is the sequence of events:
1) The application starts.
2) A WM_PAINT
message is posted to the message queue.
3) The application initialization code asserts for some reason.
4) The assertion calls MessageBox()
, which will immediately processes the WM_PAINT
message and call the application’s handler (OnDraw()
in MFC).
5) Since the application has not finished initializing, the WM_PAINT
handler issues another assertion.
6) The first assertion is completely hidden by this new assertion.
When you step into the debugger, you are now on the second assertion inside the WM_PAINT message handler – not the original assertion that is the actual source of the problem.
Reproducing The Problem
If you want to see this problem in action, it is simple to reproduce.
1) In Visual Studio, create a standard MFC application.
2) In the app class's InitInstance()
, after the line m_pMainWnd->ShowWindow(SW_SHOW)
, add _ASSERT(0)
.
3) In the view class's OnDraw()
, add _ASSERT(0)
as the first line.
4) Run the application.
Microsoft’s Solution
Microsoft has somewhat addressed the recursion problem in _VCrtDbgReportA()
(called by the _ASSERT
macros). But their solution leaves a lot to be desired.
NOTE: There is no check for recursion at all in the CRT _assert()
API – another reason to avoid it.
One problem with Microsoft’s solution is that, if you are running outside a debugger, the 2nd assertion does not put up the standard assertion dialog, but instead puts up the generic and cryptic “… application has encountered a problem and needs to close” dialog. If you press the “Debug” button, you get the Just-In-Time Debugger dialog which incorrectly claims that there has been an unhandled win32 exception.
Now that you are in the debugger, if you have the CRT source code and symbols, the debugger will position you in _CrtDbgBreak()
(in dbgrptt.c) – which is even more confusing! Without the CRT symbols, at least the debugger postitions you on the 2nd _ASSERT()
(in the WM_PAINT
handler).
There is also a “Second chance assertion” message sent to the output window. But, this is easy to miss with all of the other messages that are there.
The Better Solution
Since a Windows message pump only processes messages for the current thread, the easiest way to solve these problems is to put the assertion dialogs on their own thread and freeze the calling thread until the dialogs are closed.
To this end, I have written static class CMessageBoxThread
(MessageBoxThread.h). Since all of the methods are inline, you only need this 1 header file. You can #include it in your stdafx.h to replace all _ASSERT
and _RPTx
macros in your entire project. That's really all there is to it. You don't need to change any of your code.
I have also included a general purpose, threaded MessageBox replacement that can be used to display a standard MessageBox when you don’t want the calling thread to continue processing messages.
Example:
CMessageBoxThread::MessageBox(_T("This is the msg box text"), _T("Message box caption"));
About The Author
Curt Dixon has been a software developer in the Atlanta, Georgia area since 1983. He can be reached at curtd2004-code@yahoo.com