Thinking in C++ Vol 2 - Practical Programming |
Prev |
Home |
Next |
The condition in the Hi-Lo program depends on user input, so
you can t prevent a violation of the invariant. However, invariants usually depend
only on the code you write, so they will always hold if you ve implemented your
design correctly. In this case, it is clearer to make an assertion, which is a positive statement that reveals your design decisions.
Suppose you are implementing a vector of integers: an
expandable array that grows on demand. The function that adds an element to the
vector must first verify that there is an open slot in the underlying array
that holds the elements; otherwise, it needs to request more heap space and
copy the existing elements to the new space before adding the new element (and
deleting the old array). Such a function might look like the following:
void MyVector::push_back(int x) {
if(nextSlot == capacity)
grow();
assert(nextSlot < capacity);
data[nextSlot++] = x;
}
In this example, data is a dynamic array of ints
with capacity slots and nextSlot slots in use. The purpose of grow( )
is to expand the size of data so that the new value of capacity
is strictly greater than nextSlot. Proper behavior of MyVector
depends on this design decision, and it will never fail if the rest of the
supporting code is correct. We assert the condition with the assert( )
macro, which is defined in the header <cassert>.
The Standard C library assert( ) macro is brief,
to the point, and portable. If the condition in its parameter evaluates to
non-zero, execution continues uninterrupted; if it doesn t, a message
containing the text of the offending expression along with its source file name
and line number is printed to the standard error channel and the program
aborts. Is that too drastic? In practice, it is much more drastic to let
execution continue when a basic design assumption has failed. Your program
needs to be fixed.
If all goes well, you will thoroughly test your code with
all assertions intact by the time the final product is deployed. (We ll say
more about testing later.) Depending on the nature of your application, the
machine cycles needed to test all assertions at runtime might be too much of a
performance hit in the field. If that s the case, you can remove all the
assertion code automatically by defining the macro NDEBUG and rebuilding
the application.
To see how this works, note that a typical implementation of
assert( ) looks something like this:
#ifdef NDEBUG
#define assert(cond) ((void)0)
#else
void assertImpl(const char*, const char*, long);
#define assert(cond) \
((cond) ? (void)0 : assertImpl(???))
#endif
When the macro NDEBUG is defined, the code decays to
the expression (void) 0, so all that s left in the compilation stream is
an essentially empty statement as a result of the semicolon you appended to
each assert( ) invocation. If NDEBUG is not defined, assert(cond)
expands to a conditional statement that, when cond is zero, calls a
compiler-dependent function (which we named assertImpl( )) with a
string argument representing the text of cond, along with the file name
and line number where the assertion appeared. (We used ??? as a place holder
in the example, but the string mentioned is actually computed there, along with
the file name and the line number where the macro occurs in that file. How
these values are obtained is immaterial to our discussion.) If you want to turn
assertions on and off at different points in your program, you must not only #define
or #undef NDEBUG, but you must also re-include <cassert>.
Macros are evaluated as the preprocessor encounters them and thus use whatever NDEBUG
state applies at the point of inclusion. The most common way to define NDEBUG
once for an entire program is as a compiler option, whether through project
settings in your visual environment or via the command line, as in:
Most compilers use the D flag to define macro names.
(Substitute the name of your compiler s executable for mycc above.) The
advantage of this approach is that you can leave your assertions in the source
code as an invaluable bit of documentation, and yet there is no runtime
penalty. Because the code in an assertion disappears when NDEBUG is
defined, it is important that you never do work in an assertion. Only
test conditions that do not change the state of your program.
Whether using NDEBUG for released code is a good idea
remains a subject of debate. Tony Hoare, one of the most influential computer
scientists of all time, has
suggested that turning off runtime checks such as assertions is similar to a
sailing enthusiast who wears a life jacket while training on land and then
discards it when he goes to sea. If
an assertion fails in production, you have a problem much worse than
degradation in performance, so choose wisely.
Not all conditions should be enforced by assertions. User
errors and runtime resource failures should be signaled by throwing exceptions,
as we explained in detail in Chapter 1. It is tempting to use assertions for
most error conditions while roughing out code, with the intent to replace many
of them later with robust exception handling. Like any other temptation, use
caution, since you might forget to make all the necessary changes later.
Remember: assertions are intended to verify design decisions that will only
fail because of faulty programmer logic. The ideal is to solve all assertion
violations during development. Don t use assertions for conditions that aren t
totally in your control (for example, conditions that depend on user input). In
particular, you wouldn t want to use assertions to validate function arguments;
throw a logic_error instead.
The use of assertions as a tool to ensure program
correctness was formalized by Bertrand Meyer in his Design by Contract methodology. Every
function has an implicit contract with clients that, given certain preconditions, guarantees certain postconditions. In other words, the preconditions
are the requirements for using the function, such as supplying arguments within
certain ranges, and the postconditions are the results delivered by the
function, either by return value or by side-effect.
When client programs fail to give you valid input, you must
tell them they have broken the contract. This is not the best time to abort the
program (although you re justified in doing so since the contract was
violated), but an exception is certainly appropriate. This is why the Standard
C++ library throws exceptions derived from logic_error, such as out_of_range. If there are
functions that only you call, however, such as private functions in a class of
your own design, the assert( ) macro is appropriate, since you have
total control over the situation and you certainly want to debug your code
before shipping.
A postcondition failure indicates a program error, and it is
appropriate to use assertions for any invariant at any time, including the
postcondition test at the end of a function. This applies in particular to
class member functions that maintain the state of an object. In the MyVector
example earlier, for instance, a reasonable invariant for all public member
functions would be:
assert(0 <= nextSlot && nextSlot <=
capacity);
or, if nextSlot is an unsigned integer, simply
assert(nextSlot <= capacity);
Such an invariant is called a class invariant and can reasonably be enforced by an assertion. Subclasses play the role of subcontractor
to their base classes because they must maintain the original contract between the
base class and its clients. For this reason, the preconditions in derived
classes must impose no extra requirements beyond those in the base contract,
and the postconditions must deliver at least as much.
Validating results returned to the client, however, is
nothing more or less than testing, so using post-condition assertions in
this case would be duplicating work. Yes, it s good documentation, but more
than one developer has been fooled into improperly using post-condition
assertions as a substitute for unit testing.
Thinking in C++ Vol 2 - Practical Programming |
Prev |
Home |
Next |