Assertions in C++

Instructor's Guide


intro polymorphism idioms patterns events summary, Q/A, literature
Whatever support a language may offer, reliable software is to a large extent the result of a disciplined approach to programming. The use of assertions has long since been recognized as a powerful way in which to check whether the functional behavior of a program corresponds with its intended behavior. In effect, many programming language environments support the use of assertions in some way. For example, both C and C++ define a macro assert which checks for the result of a boolean expression and stops the execution if the expression is false.

In the example below, assertions are used to check for the satisfying of both the pre- and post-conditions of a function that computes the square root of its argument, employing a method known as Newton iteration.


  double sqrt( double arg ) { 
sqrt
require ( arg >= 0 ); double r=arg, x=1, eps=0.0001; while( fabs(r - x) > eps ) { r=x; x=r-((r*r-arg)/(2*r)); } promise ( r - arg * arg <= eps ); return r; }

slide: Using assertions in C++

In the example, the macro assert has been renamed require and promise to indicate whether the assertion serves as, respectively, a pre- or post-condition. As the example above shows, assertions provide a powerful means by which to characterize the behavior of functions, especially in those cases where the algorithmic structure itself does not give a good clue as to what the function is meant to do. The use of assertions has been promoted in  [Meyer88] as a design method for object-oriented programming in Eiffel. The idea is to define the functionality of the various methods by means of pre- and post-conditions stating in a precise manner the requirements that clients of an object must meet and the obligations an object has when executing a method. Together, the collection of methods annotated with pre- and post-conditions may be regarded as a contract between the object and its potential clients. See section contracts.

Whereas Eiffel directly supports the use of assertions by allowing access to the value of an instance variable before the execution of a method through the keyword old, the C++ programmer must rely on explicit programming to be able to compare the state before an operation with the state after the operation.


class counter { 
counter
public: counter(int n = 0) : _n(n) { require( n >= 0 ); promise( invariant() );
check initial state
} virtual void operator++() { require( true );
empty pre-condition
hold();
save the previous state
_n += 1; promise( _n == old_n + 1 && invariant() ); } int value() const { return _n; }
no side effects
virtual bool invariant() { return value() >= 0; } protected: int _n; int old_n; virtual void hold() { old_n = n; } };

slide: The counter contract

The annotated counter above includes a member function hold to store the value of its instance variable. It is used in the operator++ function to check whether the new value of the counter is indeed the result of incrementing the old value.

Assertions may also be used to check whether the object is correctly initialized. The pre-condition stated in the constructor requires that the counter must start with a value not less than zero. In addition, the constructor checks whether the class invariant, stated in the (virtual) member function invariant, is satisfied. Similarly, after checking whether the post-condition of the operator++ function is true, the invariant is checked as well.


class bounded : public counter { 
bounded
public: bounded(int b = MAXINT) : counter(0), max(b) {} void operator++() { require( value() < max );
to prevent overflow
counter::operator++(); } bool invariant() { return value() <= max && counter::invariant(); } private: int max; };

slide: Refining the counter contract

When employing inheritance, care must be taken that the invariance requirements of the base class are not violated. The class bounded, given above, refines the class counter by imposing an additional constraint that the value of the (bounded) counter must not exceed some user-defined maximum. This constraint is checked in the invariant function, together with the original counter::invariant(), which was declared virtual to allow for overriding by inheritance. In addition, the increment operator++ function contains an extra pre-condition to check whether the state of the (bounded) counter allows it to perform the operation.

From a formal perspective, the use of assertions may be regarded as a way of augmenting the type system supported by object-oriented languages. More importantly, from a software engineering perspective, the use of assertions is a means to enforce contractual obligations.