Debugging Assertions

An assertion statement specifies a condition at some particular point in your program. Visual C++ supports assertion statements based on the following constructs:

Programs that use the MFC library should use the MFC ASSERT macro. Programs that use the run-time library should use the runtime _ASSERT macro. Other programs should use the ANSI assert function.

Assertion statements compile only when _DEBUG is defined. When _DEBUG is not defined, the compiler treats assertions as null statements. Therefore, assertion statements have zero overhead in your final release program; you can use them liberally in your code without affecting the performance of your release version.

Note   The expression argument to ASSERT is not evaluated in the release version of your program. If you want the expression to be evaluated in both debug and release environments, use the VERIFY macro instead of ASSERT. In release environments, VERIFY evaluates the expression argument but does not check the result.

Catching Errors with Assertion Statements

Assertion statements are useful for catching logic errors. If you set an assertion on a condition that must be true according to the logic of your program, the assertion has no effect unless a logic error occurs. For example, suppose you are writing a simulation of gas molecules in a container, and the variable numMols represents the total number of molecules. Obviously, this number cannot be less than zero, so you might include an MFC assertion statement like this:

ASSERT(numMols >= 0);

This statement does nothing if your program is operating correctly. If a logic error has caused numMols to be less than zero, however, the statement halts the execution of your program and displays the Assertion Failed dialog box. This dialog box has three buttons, with the functions described in the following table.

Click this button To do this
Retry Debug the assertion or get help on asserts.
Ignore Ignore the assertion and continue running the program.
Abort Halt execution of the program and end the debugging session.

When the debugger halts because of an MFC or C run-time library assertion, it navigates to the point in the source file where the assertion occurred, if the source is available. The Debug tab of the Output window displays the assertion message that appeared in the Assertion Failed dialog box. If you want to keep a copy of the message for future reference, you can copy it from the Output window to a text window using copy and paste or dragging it. The Output window may contain other error messages as well. Examine these carefully; some may provide clues to the cause of the assertion failure.

When you add assertions to your code, avoid writing assertions that have side effects. For example:

ASSERT(numMols++ > 0); -- Don’t do this!

This assertion statement changes the value of numMols. If you write assertion statements that have side effects like this, the debug and release versions of your code will produce different results, because the side effects occur only when _DEBUG is defined. Be careful using assertion statements on library or system calls, which may have side effects.

Checking Results with Assertion Statements

Use assertion statements when you need to check the result of an operation. Assertions are most valuable for testing operations whose results are not obvious from quick visual inspection. Consider, for example, the following code, which updates the variable iMols based on the contents of the linked list pointed to by mols:

while (mols->type <> “H2O”)
{
 iMols += mols->num;
 mols = mols->next;
}
ASSERT(iMols<=numMols);

The number of molecules counted by iMols must always be less than or equal to the total number of molecules, numMols. A visual inspection of this loop does not guarantee that this must be the case, so an assertion statement is used after the loop to test for that condition.

Testing Error Conditions with Assertion Statements

Another use of assertion statements is to test for error conditions. Assertion statements are not a substitute for error-handling code, however. The following example shows an assertion statement that can lead to problems in the final release code:

myErr = myGraphRoutine(a, b);
ASSERT(!myErr); -- Don’t do this!

This code relies on the assertion statement to handle the error condition. As a result, any error code returned by myGraphRoutine will be unhandled in the final release code.

You can use assertion statements to check for error conditions at a point in your code where any errors should have been handled by preceding code. In the following example, a graphic routine returns zero if it succeeds and an error code if an error (such as running out of memory) occurs. You can use an MFC assertion statement as follows:

myErr = myGraphRoutine(a, b);
...
/* Code to handle errors and 
   reset myErr if successful */
...
ASSERT(!myErr);

If the error-handling code works properly, any error that occurs is handled, and myErr is restored to a value of zero, indicating no error, before the assertion statement is reached. The assertion succeeds, and control passes to the next statement. However, if myErr has another value, the assertion fails, the program halts, and the Assertion Failed dialog box appears.

Examining Assertion Failures

When an assertion fails, you must examine your program to determine the cause of the failure. If the assertion statement and the cause of the failure occur close to one another, debugging can be relatively straightforward. Sometimes, however, an assertion failure may provide little or no clue as to where the cause is located. For example, suppose your code contained the following assertion statement:

ASSERT(ialloc %50 == 0);

The program in this example allocates memory in blocks of 50 bytes. This assertion tests to see that memory is allocated in multiples of the proper size. If the assertion fails, you know that a memory leak exists in your program. Your next task, of course, is to find out where.

To isolate the location where the memory leak occurs, you might add copies of this assertion statement to other parts of your program where memory is allocated. Another alternative is to set a data breakpoint.

To find the locations where a condition fails

  1. From the Edit menu, choose Breakpoints.

    The Breakpoints dialog box appears.

  2. Choose the Data tab.

  3. In the Enter The Expression To Be Evaluated text box, type the negation of the expression that caused the asertion failure.

    In general, for ASSERT (anyExpression), you can specify a data breakpoint as !(anyExpression).

    For example, if your program failed on the assertion:

    ASSERT(ialloc %50 == 0);
    

    In the Breakpoints dialog box, you would type:

    !(ialloc %50 == 0)
    
  4. Run your program again.

    Execution halts when the condition specified in the data breakpoint becomes true. This is the point where the condition in the assertion statement becomes false.

Finding Assertion Failures with the Call Stack Window

Another tool that is often useful for finding the cause of an assertion failure is the Call Stack window. Using the Call Stack window, you can examine previous functions and look for problems that may have caused the failure.

To navigate from the Call Stack window to a function’s source or object code

  1. From the View menu, click Debug Windows and Call Stack.

    The Call Stack window opens.

  2. In the Call Stack window, double-click the function name.

    – or –

    Select the function name, and press ENTER.

On rare occasions, you might want to look at the assertion-handling code rather than the code that caused the assertion to fail. You can use the Call Stack window for that purpose as well.