Assert Statements Shine Light Into Dark Corners
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.
White PapersMore >>
- Enabling a Smooth DX Transformation in the Post-Pandemic New Tomorrow
- [Free Virtual Event] Using Microsoft in 2020 & Beyond
We used as an example last week a
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
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
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
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.
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.
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.
In effect, by catching invariant failures as they happen,
assert makes programs more likely to produce correct results — if they produce results at all. This behavior leaves unanswered the question of how one deals with the failed invariants. We shall discuss strategies for doing so next time.