Выбрать главу

Assertions

The condition in the Hi-lo program depends on user input, so you’re powerless to always prevent a violation of the invariant. Most often, however, invariants 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.

For example, suppose you are implementing a vector of integers, which, as you know, is 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 of course 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, so we assert the condition with the assert( ) macro (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 have thoroughly tested 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 not only have to #define or #undef NDEBUG, but you have to re-include <cassert>. Macros are evaluated as the preprocessor encounters them and therefore use whatever NDEBUG state applies at that point in time. 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

mycc –DNDEBUG myfile.cpp

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,[13] 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 actually goes to sea.[14] 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, of course. 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.[15] Every function has an implicit contract with clients that, given certain pre-conditions, guarantees certain post-conditions. In other words, the pre-conditions are the requirements for using the function, such as supplying arguments within certain ranges, and the post-conditions are the results delivered by the function, either by return value or by side-effect.

What should you do when clients fail to give you valid input? They have broken the contract, and you need to let them know. As we mentioned earlier, 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 in order. This is why the Standard C++ library throws exceptions derived from logic_error, such as out_of_range.[16] 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.

вернуться

13

Among other things he invented Quicksort.

вернуться

14

As quoted in Programming Language Pragmatics, by Michael L. Scott, Morgan-Kaufmann, 2000.

вернуться

15

See his book, Object-Oriented Software Construction, Prentice-Hall, 1994.

вернуться

16

This is still an assertion conceptually, but since we don’t want to halt execution, the assert( ) macro is not appropriate. Java 1.4, for example, throws an exception when an assertion fails.