Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Design by Contract in C++

0.00/5 (No votes)
15 Sep 2004 3  
An article decribing how to implement Design by Contact in C++, using lambda expressions.

Introduction

Every now and then I see attempts at introducing the Design by Contract (DbC) concept into C++, usually using some kind of assertions. There are limits on how well that can work, but I thought that it should be possible to do it better than what I've seen to date.

DbC is usually used to describe three kinds of assertions:

  • Precondition - a condition that must hold upon invocation of a function.
  • Postcondition - a condition that must hold upon exit from a function.
  • Class invariant - a condition that must always hold for objects of the class, except while a public member function is executing.

DbC was introduced to the programming community in the book Object-Oriented Software Construction [1], and is an integrated part of the Eiffel programming language. Few (any?) mainstream languages support it out of the box though, certainly neither C++, C#, nor Java does. Of those three, C++ lends itself the best to the task of implementing DbC, thanks to scope-based object destruction and templates, two features which are lacking in both C# and Java.

The problem

Basically, for a given function, we want to specify a precondition, that needs to hold at the entry into the function, and a postcondition, that needs to hold at the exit of a function, whether by explicit return or by stack unwinding from an uncaught exception. Also, we want to be able to specify a class invariant condition that is checked both at entry and exit from any public member function.

There are limits to what can be done. In particular, it won't be possible to hinder users from overriding virtual functions with implementations that don't fulfill the pre- or postconditions stated in the base function. And the invariant condition cannot be checked automatically; some kind of trigger must be provided by the programmer. None the less, there are a lot of facilities in C++ at our disposal, and we can go quite a distance with them.

The main problem is to specify a postcondition at the beginning of a function, while delaying the actual check until the function is exiting. Destructors of local stack-allocated objects are the key to that problem, together with lambda expressions. We'll also borrow some ideas from [2] to provide useful debugging information for failed conditions. Using the preprocessor, we can put a nice veneer on top of all the machinery. The following example shows what we end up with.

Example

// The Rectangle class represents a rectangular

// viewing area that can be zoomed or translated.

// The rectangle is specified by two corners,

// of which the first must have smaller

// coordinates than the second.


#include "PrePostCondition.h"


namespace Demo {

    class Rectangle
    {
    public:
        Rectangle(double x1, double y1, double x2, double y2) :
            x1(x1), 
            y1(y1), 
            x2(x2), 
            y2(y2)
        {
            PRECONDITION(1);
            POSTCONDITION(1);
        }

        double width()
        {
            return x2-x1;
        }

        double height()
        {
            return y2-y1;
        }

        void zoom(double factor)
        {
            PRECONDITION( factor > 0 )(factor);
            POSTCONDITION( POST(width) == 
                width()*factor )(width())(factor)(POST(width));
            POSTCONDITION( POST(height) == 
                height()*factor )(height())(factor)(POST(height));

            // Implementation

            // ...

        }

        void translate(double dx, double dy)
        {
            PRECONDITION(1);
            POSTCONDITION( POST(x1) == x1+dx )(x1)(POST(x1))(dx);
            POSTCONDITION( POST(y1) == y1+dy )(y1)(POST(y1))(dy);
            POSTCONDITION( POST(x2) == x2+dx )(x2)(POST(x2))(dx);
            POSTCONDITION( POST(y2) == y2+dy )(y2)(POST(y2))(dy);

            // Implementation

            // ...


        }

    private:
        double x1;
        double y1;
        double x2;
        double y2;

        BEGIN_INVARIANT(Rectangle)
            DEFINE_INVARIANT( x2 > x1 && y2 > y1 )(x1)(x2)(y1)(y2)
        END_INVARIANT()
    };

} // namespace Demo


int main()
{
    Demo::Rectangle badRectangle(0,0,-1,-1);
    Demo::Rectangle rectangle(0,0,1,1);
    rectangle.zoom(-1);
    rectangle.translate(1,1);
    rectangle.zoom(2);
    rectangle.translate(-5,-5);
    rectangle.zoom(.5);
    return 0;
}

The POST() macro indicates that the argument should not be evaluated until the condition itself is evaluated; without the POST() the argument will be evaluated immediately. The argument can be a variable, free function, or member function.

Both the PRECONDITION and POSTCONDITION macros will check the class invariant, one at entry and the other at exit. When a precondition, postcondition, or invariant fails, the relevant information is printed to the debug output. To see this, compile and run the example code. Since we've neglected to supply implementations for the zoom and translate functions, and on top of that we've had the temerity to create a Rectangle with bad corners, we'll be hit by a slew of failed preconditions, postconditions, and invariants. I would have liked to put a screenshot here of the output window in Visual Studio, showing all the failures, but this article is limited to 600 pixel wide images, and you wouldn't really see anything. In any case, the full text of one of the lines in the output window is:

e:\development\codeproject\designbycontract\demo.cpp(34): FAILURE: 
Postcondition: POST(height) == height()*factor in Demo::Rectangle::zoom: 
Thread-id=2572 : Timestamp(ms)= 35866: height()=1, factor= -1, POST(height)=1,

Here's another one:

e:\development\codeproject\designbycontract\demo.cpp(16): FAILURE: Class 
invariant (on exit): x2 > x1 && y2 > y1 in 
Demo::Rectangle::Rectangle: Thread-id=2572 : 
      Timestamp(ms)=37800: x1=0, x2=-1, y1=0, y2=-1,

Double-clicking these lines will bring you straight to the source code line of the failed condition. Note how easy it is to write out extra variables and values to aid in diagnosing the situation.

The actions to be taken after a failed condition is easily customizable. The default action is to do nothing, i.e. continue with normal execution. A more noticeable action that springs to mind is to trigger an assertion in debug builds, and to throw an exception in release builds. To enable this failure action, redefine the PRECONDITION and POSTCONDITION macros:

#include "PrePostCondition.h" 


#undef PRECONDITION
#undef POSTCONDITION

#ifdef NDEBUG 
#define PRECONDITION(condition) 
        UTIL_PRECONDITION( util::PrePostCondition::Print_ODS, 
        util::PrePostCondition::Fail_Throw, condition ) 
#define POSTCONDITION(condition) 
        UTIL_POSTCONDITION( util::PrePostCondition::Print_ODS, 
        util::PrePostCondition::Fail_Throw, condition ) 
#else 
#define PRECONDITION(condition) 
        UTIL_PRECONDITION( util::PrePostCondition::Print_ODS, 
        util::PrePostCondition::Fail_Assert, condition ) 
#define POSTCONDITION(condition) 
        UTIL_POSTCONDITION( util::PrePostCondition::Print_ODS, 
        util::PrePostCondition::Fail_Assert, condition ) 
#endif

If a condition failure is detected during a stack unwind from an uncaught exception, then no action is taken; throwing another exception in such cases would unceremoniously terminate the whole program.

Implementation details

We use destructors of local variables to delay the checking of postconditions and invariants. In order to store the expressions constituting the condition, we make use of the Boost Lambda Library [3], which allows us to build complex expressions that can be stored and evaluated at some latter point in time. To allow users to output extra information when a condition fails, we apply some preprocessor magic from [2], creating what in effect is a variable argument macro.

Because of the heavy use of lambda expressions, there is a noticeable increase in compilation time when using these macros. I haven't tried to optimize the framework with respect to compile-time; currently there is an abundance of functors and compile-time lambda expressions, obviating a number of these would definitely help the compilation time. Using precompiled headers and instantiating common templates there also helps.

The runtime penalty is also significant, especially in debug builds since no inlining is performed. This is generally less of an issue though. While testing a program, we can put up with a performance loss to help localize DbC violations, and once a program is determined to be running correctly, the checking can be disabled.

The included sample program has been compiled and tested with Visual C++ 7.1. It almost surely won't work with earlier versions of Visual C++, due to compiler idiosyncrasies (a.k.a. bugs). The Boost Function and Lambda libraries are required, and may be downloaded as part of the Boost library [4]. I've tested against versions 1.30 and 1.31 of Boost.

Summary

I've presented a C++ framework for implementing DbC, which allows class invariants, preconditions, and postconditions to be stated up front and enforced at the correct times, with extensive diagnostic information when a failure is encountered.

Comments are gratefully received. If you come across some bugs, please tell me!

Bibliography

[1] - Bertrand Meyer: Object-Oriented Software Construction, Second Edition, Prentice Hall, 1997.

[2] - Andrei Alexandrescu, John Torjo: Enhancing Assertions, C++ Users Journal, August 2003.

[3] - Jaakko J�rvi, Gary Powell: Boost Lambda Library.

[4] - www.boost.org

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here