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.
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
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
The Problem With Assertions -- Unintentional Recursion
Assertions usually put up a dialog box, which is created with the Windows API
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.
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
3) In the view class's
_ASSERT(0) as the first line.
4) Run the application.
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
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
_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.
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 firstname.lastname@example.org