Programmers draw a distinction between exceptions and bugs. An exception is a predictable untoward result of conditions that is beyond the control of the programmer, and your program must handle the problem gracefully. Some examples of exceptions are:
A bug is a failure in the code itself. Bugs must be identified and removed before shipping the product, if at all possible. One way to identify bugs is to articulate and document your assumptions at every step of the program. If your program is only valid if a pointer is non-null — if it is a bug for that pointer to be null at this particular step — then this assumption must be validated. You validate this assumption using ASSERT
.
Let's start by defining the macro. The ASSERT()
macro takes a Boolean expression as a parameter. If the expression evaluates to FALSE
, then it takes some kind of action, but does nothing if it evaluates to TRUE
. Many compilers will abort the program on an ASSERT()
that fails, whereas others will throw an exception.
ASSERT()
macros are only used in debug mode, and must collapse into nothing when Debug is not defined. This allows you use assert macros aggressively when building your program without paying a performance penalty in the release version.
While you would normally use the ASSERT()
macro provided by your compiler, it can be helpful to write your own, if only to understand in some detail what the macro does. Here's a simple ASSERT()
macro I've used in debugging applications, using standard input and output (non-windows applications):
#define DEBUG
#ifndef DEBUG
#define ASSERT(x)
#else
#define ASSERT(x) \
if (! (x)) \
{ \
cout << "Assert " << #x << " failed\n"; \
cout << " on line " << __LINE__ << "\n"; \
cout << " in file " << __FILE__ << "\n"; \
}
#endif
You can test this macro with the following code:
#include <iostream.h>
int main()
{
int x = 5;
cout << "First assert: \n";
ASSERT(x==5);
cout << "\nSecond assert: \n";
ASSERT(x != 5);
cout << "\nDone.\n";
return 0;
}
If you try out this code you should see the following output:
First assert:
Second assert:
Assert x !=5 failed
on line 24
in file myTest.cpp
Done.
As you can see, the first time we call ASSERT()
the result is TRUE
, and the macro does nothing. The second time we call ASSERT()
, however, the result is FALSE
, and the program prints out the line number and file name where ASSERT()
failed. Microsoft's Visual C++ 5 provides an assert macro which allows you to abort the program, ignore the ASSERT()
or enter the debugger at the line where the assert failed. (Debugging is discussed in some detail towards the end of this chapter. The source code is the same but the output is quite different.
BOOL CMyTestApp::SomeFunction()
{
int x = 5;
cout << "First assert: \n";
ASSERT(x==5);
cout << "\nSecond assert: \n";
ASSERT(x != 5);
cout << "\nDone.\n";
return 0;
}
This window provides the necessary information to decide whether to continue past the ASSERT()
or to examine the code at the point of failure to see why the assertion failed.
I have no idea why Microsoft thinks the right button name for debugging the application is Retry. This dialog would be much easier to understand if the buttons were Abort, Debug and Ignore — but perhaps they were able to save a line or two of code by reusing this trilogy of choices.
Asserts do more than just allow you to test your assumptions — they document those assumptions in the body of your code. Examine these two blocks of code:
SomeClass* pSomePointer = SomeFunction();
if ( pSomePointer)
pSomePointer->SomeMethod();
SomeClass* pSomePointer = SomeFunction();
ASSERT ( pSomePointer );
pSomePointer->SomeMethod();
There are no comments on either block, yet each speaks volumes about SomeFunction()
and what the programmer expects. In the first case, SomeFunction()
obviously returns a pointer to SomeClass
, at least some of the time. The programmer is careful to test the pointer before using it, and if the pointer is NULL
, the call to SomeMethod()
is skipped.
Note that in the second line of this code snippet, I test pSomePointer
as if it were a boolean. This common C/C++ idiom relies on the fact that a non-null pointer is treated as TRUE
by the compiler. It would be more explicit (but less macho/cool/hip) to write:
if ( pSomePointer != NULL )
In the second block, the programmer clearly expects that SomeFunction()
always returns a valid pointer. He asserts that the pointer is valid, but if it is NULL
, this is a programming bug. If SomeFunction()
might ever legally return a null pointer, then this block of code would reflect a very poor design; but if , on the other hand, it is in fact a violation of the contract for SomeFunction()
to return NULL
, then this body of code is perfectly valid.
In this case, the ASSERT()
macro is serving a double duty; it not only tests the pointer, but it also documents the programmer's assumptions about the SomeFunction()
call. There is no need to add the comment,
// SomeFunction() always returns a valid pointer
SomeClass * pSomePointer = SomeFunction();
ASSERT ( pSomePointer );
pSomePointer->SomeMethod();
because the assert provides sufficient documentation of the assumption.