Assert Statements Shine Light Into Dark Corners Let's explore the consequences of checking that invariants actually hold when they should — a state of affairs that in principle should always be true.



Last week, I introduced the notion of an invariant. Invariants are an unusual concept because our programs usually do not check them. Instead, invariants are intellectual tools that programmers use to think about how their programs work. We continue by exploring the consequences of checking that invariants actually hold when they should — a state of affairs that in principle should always be true.

We used as an example last week a vector , v , with elements that we assume are sorted. We explained that because the elements were sorted, we could use a binary-search algorithm to find values in the vector , and in exchange for that convenience, we had to ensure that the elements actually were sorted whenever we were done working with them.

What we didn't talk about was what happens if the vector somehow winds up getting out of sequence, and how to detect that it has done so. If our code is written correctly, that should never happen — which, of course, is why we feel free about writing code in the first place that assumes that the vector is in sequence. However, precisely because we believe such conditions should never happen, we tend not to think about that possibility. As a result, when invariants do turn out to be false during program execution, the result often appears as a failure in what looks at first like an unrelated part of the program. We call such a situation an invariant failure; such failures can be very hard to trace.

Among the easiest ways to avoid invariant failures is to use assert statements to detect them. Technically speaking, the assert statement in C++ is a preprocessor macro, not a statement, but it behaves similarly to a statement. It takes the form

assert(expression);

and either tests the expression or does nothing, depending on whether the preprocessor macro NDEBUG is defined at the point in the program that contains the assert . If NDEBUG is not set, the expression is tested; if the test yields false (i.e., zero), the entire program is terminated.

The idea, then, is that the NDEBUG macro is used to turn off "debugging mode," and if a program is compiled in debugging mode, encountering an assert statement verifies that the expression given as its argument is true. So, for example, we can write a statement such as

assert(is_sorted(v));

before we try to use a binary-search algorithm on v . If the program is compiled in debugging mode, the assert will call is_sorted , which can check whether v is actually sorted. If, on the other hand, the program is compiled in production mode, the entire assert statement does nothing.

Using is_sorted in this way is a good example of why assert is useful. We do not want to call is_sorted every time we use v , because if we could afford to do so, we could probably also have chosen algorithms that do not require v to be sorted at all. On the other hand, being able to turn on debug mode and have is_sorted called for us is useful: Whenever the program is misbehaving in ways that "can't happen," we can turn on debug mode, recompile the program, and quickly learn whether the problem is an invariant failure.

Because assert is implemented as a macro, turning on NDEBUG completely eliminates the code that tests the condition. This behavior has the obvious advantage that there is no overhead attached to using assert when it is not needed. However, there is a more subtle advantage to using assert rather than exceptions to handle invariant failure: An invariant failure is a clear sign that the program is broken. If you like, the point of assert is to handle situations that "can't happen." When such a situation happens anyway, it's hard to say what code that catches an exception might do, because that code might rely on information that is wrong because of the invariant failure.

Despite the foregoing discussion, there is one big disadvantage to using assert : When an assert fails, the program terminates. This property of assert is hard to accommodate in systems that need to keep running. It's unacceptable for a program such as a word processor to terminate with an assertion failure simply because the user happened to do something that triggered a previously undetected bug. And yet the whole reason for using an assert is to ensure that the program does not quietly continue producing nonsense results because the conditions that it expected to find do not exist.