Follow Techotopia on Twitter

On-line Guides
All Guides
eBook Store
iOS / Android
Linux for Beginners
Office Productivity
Linux Installation
Linux Security
Linux Utilities
Linux Virtualization
Linux Kernel
System/Network Admin
Programming
Scripting Languages
Development Tools
Web Development
GUI Toolkits/Desktop
Databases
Mail Systems
openSolaris
Eclipse Documentation
Techotopia.com
Virtuatopia.com
Answertopia.com

How To Guides
Virtualization
General System Admin
Linux Security
Linux Filesystems
Web Servers
Graphics & Desktop
PC Hardware
Windows
Problem Solutions
Privacy Policy

  




 

 

Thinking in C++ Vol 2 - Practical Programming
Prev Home Next

Assertions

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:

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,[15] 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.[16] 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.[17] 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.[18] 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.[19]

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

 
 
   Reproduced courtesy of Bruce Eckel, MindView, Inc. Design by Interspire