Made to Order Software Corporation Logo

The assert() macro in C/C++

 

This is a subject that comes back all the time in C/C++ boards.

Should you use assertions?

The answer is clearly yes. But the C/C++ assert() function is usually defined using a macro. Macros have several problems. The most common ones are: they offer no type checking, they do not warn you about weird side effects, they have a different syntax than the C/C++ language itself.

One good thing: for a fast program, the debug code used to check parameters, results, etc. is gone.

One really bad thing: if the expression in the macro has a side effect, the release program is different from the debug version. This is the hardest one to temper with since you use the debug version to debug the program. The debug version works. And once you build and run the release version, it breaks. And it usually takes time to realize the mistake…

#ifdef _DEBUG
#define assert(x) if(!x) abort();
#else
#define assert(x) /* nothing in release */
#endif

One of the main idea behind the Ada language is to avoid multiple versions. A good Ada compiler does not offer you flags on the command line*. The result is that you can write exactly one program, no more. There is no debug and non-debug version. This is because you do not want to debug two versions of the same program. The debug and the non-debug versions are two distinct programs. Even if you ignore the bytes representing the time that is saved in some binary files, comparing these two programs will result in a large number of differences.

*In practice, however, most Ada compilers do offer command line flags or some similar functionality! Yuck! You are instead supposed to use the pragma keyword inside your program to make it behave one way or the other.

In Ada, you can use the pragma assert() function. This is similar to the C/C++ assert, except that the no side effect rule can be applied by the language (it is not always the case, though.) However, the usual method to assert the validity of function parameters is to create strong types. Setting a parameter to the wrong value will automatically result in the language asserting at runtime, and if possible, the compiler generating an error (a static error is the best! Caught early, such errors can be fixed at once without having to debug anything.)

Other attempts have been made to fix the problem of assertions. The Eiffel language is one of them. In this case, you do not write assertions. Instead, you write conditions that have to be met for a function to be entered and another set of conditions to be met when exiting (you can return multiple variables and thus you may need multiple expressions on exit.) This is very similar to having assertions in C/C++ except that the language enforce the no side effects rule.

You may ask: “But why are we still writing two C/C++ versions?”

Simple: the debug version can be really slow because it tests all these many things that in the normally working program do not need to be tested since the software works perfectly (you just debugged it, right!?) And especially, a debug version may print a lot of information in your console, open alert windows, etc. and that’s not acceptable in the final version.

Well… Yes! If you are programming a commercial software, you are more than likely to have a debug and a non-debug version (and under MS-Windows, you may even have a Multi-byte and Unicode version!) So how can you avoid differences from showing up because the assert() expressions disappear in a release version?

In C++, one can ameliorate the assert() function by writing an actual C++ function. That way, you can ensure that even in release, you will get the same side effects as in debug mode. Only the test is removed. And when the function is declared inline, then the compiler will eventually optimize the whole thing to nothing just like with the macro. Plus, since it is your function, you can do anything you want in it such as open a window and ask the user whether to continue or not, add a message parameter to be displayed whenever the assertion fails…

inline void assert(bool x)
{
#if _DEBUG
	if(!x) abort();
#endif
}

Note that in C++ you need to repeat the function for each C++ type (bool, char, short, int, long, and unsigned’s, float, double, void*) to avoid warnings, or always make sure that your tests yield a boolean result. Now, when I call my assert in this way:

assert(i++ != 0);

the side effect of incrementing i in debug mode will stay in release mode. I just avoided a rather strange bug.

Of course, this bug is rather straight forward and most programmers will find it quickly (yet, they need to think about assert/side effects… and to actually look at the assert!)

But most of the time, bugs with asserts will involve a call to another function. That other function may be constant at first and not have any side effects. After a while, however, that function may evolve and the newer version may very well have side effects. That’s when it becomes complicated. The C++ assert() function is the best solution to avoid these problems.

In conclusion: to avoid even more bugs, use an assert function instead of a macro.

Note 1: you most certainly will have to name your assert() function something else since the system defines an assert() function already.

Note 2: in C, you can write an assert() function that accepts the largest integer. All numbers will work as is, though double floating points may be rounded and result in an assert when not really zero. Also pointers will need to be compared to NULL (i.e. ptr != NULL).