D Programming Language 2.0

- Comments
- D
- Search
- Downloads
- Home

Last update Sun Jul 1 10:45:30 2007

# D's Contract Programming vs C++'s

Many people have written me saying that D's Contract Programming (DbC) does not add anything that C++ does not already support. They go on to illustrate their point with a technique for doing DbC in C++.

It makes sense to review what DbC is, how it is done in D, and stack that up with what each of the various C++ DbC techniques can do.

Digital Mars C++ adds extensions to C++ to support DbC, but they are not covered here because they are not part of standard C++ and are not supported by any other C++ compiler.

## Contract Programming in D

This is more fully documented in the D Contract Programming document. To sum up, DbC in D has the following characteristics:

1. The assert is the basic contract.
2. When an assert contract fails, it throws an exception. Such exceptions can be caught and handled, or allowed to terminate the program.
3. Classes can have class invariants which are checked upon entry and exit of each public class member function, the exit of each constructor, and the entry of the destructor.
4. Assert contracts on object references check the class invariant for that object.
5. Class invariants are inherited, that means that a derived class invariant will implicitly call the base class invariant.
6. Functions can have preconditions and postconditions.
7. For member functions in a class inheritance hierarchy, the precondition of a derived class function are OR'd together with the preconditions of all the functions it overrides. The postconditions are AND'd together.
8. By throwing a compiler switch, DbC code can be enabled or can be withdrawn from the compiled code.
9. Code works semantically the same with or without DbC checking enabled.

## Contract Programming in C++

### The `assert` Macro

C++ does have the basic `assert` macro, which tests its argument and if it fails, aborts the program. `assert` can be turned on and off with the `NDEBUG` macro.

`assert` does not know anything about class invariants, and does not throw an exception when it fails. It just aborts the program after writing a message. `assert` relies on a macro text preprocessor to work.

`assert` is where explicit support for DbC in Standard C++ begins and ends.

### Class Invariants

Consider a class invariant in D:

    class A
    {
        invariant() { ...contracts... }

        this() { ... }	// constructor
        ~this() { ... }	// destructor

        void foo() { ... }	// public member function
    }

    class B : A
    {
        invariant() { ...contracts... }
        ...
    }

To accomplish the equivalent in C++ (thanks to Bob Bell for providing this):

    template
    inline void check_invariant(T& iX)
    {
    #ifdef DBC
        iX.invariant();
    #endif
    }

    // A.h:

    class A {
        public:
    #ifdef DBG
           virtual void invariant() { ...contracts... }
    #endif
           void foo();
    };

    // A.cpp:

    void A::foo()
    {
        check_invariant(*this);
        ...
        check_invariant(*this);
    }

    // B.h:

    #include "A.h"

    class B : public A {
        public:
    #ifdef DBG
    	virtual void invariant()
    	{   ...contracts...
    	   A::invariant();
    	}
    #endif
           void bar();
    };

    // B.cpp:

    void B::barG()
    {
        check_invariant(*this);
        ...
        check_invariant(*this);
    }

There's an additional complication with `A::foo()`. Upon every normal exit from the function, the `invariant()` should be called. This means that code that looks like:

    int A::foo()
    {
        ...
        if (...)
    	return bar();
        return 3;
    }

would need to be written as:

    int A::foo()
    {
        int result;
        check_invariant(*this);
        ...
        if (...)
        {
    	result = bar();
    	check_invariant(*this);
    	return result;
        }
        check_invariant(*this);
        return 3;
    }

Or recode the function so it has a single exit point. One possibility to mitigate this is to use RAII techniques:

    int A::foo()
    {
    #if DBC
        struct Sentry {
           Sentry(A& iA) : mA(iA) { check_invariants(iA); }
           ~Sentry() { check_invariants(mA); }
           A& mA;
        } sentry(*this);
    #endif
        ...
        if (...)
    	return bar();
        return 3;
    }

The #if DBC is still there because some compilers may not optimize the whole thing away if check_invariants compiles to nothing.

## Preconditions and Postconditions

Consider the following in D:

    void foo()
        in { ...preconditions... }
        out { ...postconditions... }
        body
        {
    	...implementation...
        }

This is nicely handled in C++ with the nested Sentry struct:

    void foo()
    {
        struct Sentry
        {   Sentry() { ...preconditions... }
    	~Sentry() { ...postconditions... }
        } sentry;
        ...implementation...
    }

If the preconditions and postconditions consist of nothing more than `assert` macros, the whole doesn't need to be wrapped in a `#ifdef` pair, since a good C++ compiler will optimize the whole thing away if the `assert`s are turned off.

But suppose `foo()` sorts an array, and the postcondition needs to walk the array and verify that it really is sorted. Now the shebang needs to be wrapped in `#ifdef`:

    void foo()
    {
    #ifdef DBC
        struct Sentry
        {   Sentry() { ...preconditions... }
    	~Sentry() { ...postconditions... }
        } sentry;
    #endif
        ...implementation...
    }

(One can make use of the C++ rule that templates are only instantiated when used can be used to avoid the `#ifdef`, by putting the conditions into a template function referenced by the `assert`.)

Let's add a return value to `foo()` that needs to be checked in the postconditions. In D:

    int foo()
        in { ...preconditions... }
        out (result) { ...postconditions... }
        body
        {
    	...implementation...
    	if (...)
    	    return bar();
    	return 3;
        }

In C++:

    int foo()
    {
    #ifdef DBC
        struct Sentry
        {   int result;
    	Sentry() { ...preconditions... }
    	~Sentry() { ...postconditions... }
        } sentry;
    #endif
        ...implementation...
        if (...)
        {   int i = bar();
    #ifdef DBC
    	sentry.result = i;
    #endif
    	return i;
        }
    #ifdef DBC
        sentry.result = 3;
    #endif
        return 3;
    }

Now add a couple parameters to `foo()`. In D:

    int foo(int a, int b)
        in { ...preconditions... }
        out (result) { ...postconditions... }
        body
        {
    	...implementation...
    	if (...)
    	    return bar();
    	return 3;
        }

In C++:

    int foo(int a, int b)
    {
    #ifdef DBC
        struct Sentry
        {   int a, b;
    	int result;
    	Sentry(int a, int b)
    	{   this->a = a;
    	    this->b = b;
    	    ...preconditions...
    	}
    	~Sentry() { ...postconditions... }
        } sentry(a,b);
    #endif
        ...implementation...
    	if (...)
    	{   int i = bar();
    #ifdef DBC
    	    sentry.result = i;
    #endif
    	    return i;
    	}
    #ifdef DBC
    	sentry.result = 3;
    #endif
    	return 3;
    }

## Preconditions and Postconditions for Member Functions

Consider the use of preconditions and postconditions for a polymorphic function in D:

    class A
    {
        void foo()
    	in { ...Apreconditions... }
    	out { ...Apostconditions... }
    	body
    	{
    	    ...implementation...
    	}
    }

    class B : A
    {
        void foo()
    	in { ...Bpreconditions... }
    	out { ...Bpostconditions... }
    	body
    	{
    	    ...implementation...
    	}
    }

The semantics for a call to `B.foo()` are:

- Either Apreconditions or Bpreconditions must be satisfied.
- Both Apostconditions and Bpostconditions must be satisfied.

Let's get this to work in C++:

    class A
    {
    protected:
        #if DBC
        int foo_preconditions() { ...Apreconditions... }
        void foo_postconditions() { ...Apostconditions... }
        #else
        int foo_preconditions() { return 1; }
        void foo_postconditions() { }
        #endif

        void foo_internal()
        {
    	...implementation...
        }

    public:
        virtual void foo()
        {
    	foo_preconditions();
    	foo_internal();
    	foo_postconditions();
        }
    };

    class B : A
    {
    protected:
        #if DBC
        int foo_preconditions() { ...Bpreconditions... }
        void foo_postconditions() { ...Bpostconditions... }
        #else
        int foo_preconditions() { return 1; }
        void foo_postconditions() { }
        #endif

        void foo_internal()
        {
    	...implementation...
        }

    public:
        virtual void foo()
        {
    	assert(foo_preconditions() || A::foo_preconditions());
    	foo_internal();
    	A::foo_postconditions();
    	foo_postconditions();
        }
    };

Something interesting has happened here. The preconditions can no longer be done using `assert`, since the results need to be OR'd together. I'll leave as a reader exercise adding in a class invariant, function return values for `foo()`, and parameters for `foo()`.

## Conclusion

These C++ techniques can work up to a point. But, aside from `assert`, they are not standardized and so will vary from project to project. Furthermore, they require much tedious adhesion to a particular convention, and add significant clutter to the code. Perhaps that's why it's rarely seen in practice.

By adding support for DbC into the language, D offers an easy way to use DbC and get it right. Being in the language standardizes the way it will be used from project to project.

## References

Chapter C.11 introduces the theory and rationale of Contract Programming in Object-Oriented Software Construction
Bertrand Meyer, Prentice Hall

Chapters 24.3.7.1 to 24.3.7.3 discuss Contract Programming in C++ in The C++ Programming Language Special Edition
Bjarne Stroustrup, Addison-Wesley

Copyright © 1999-2007 by Digital Mars, All Rights Reserved | Page generated by Ddoc. | Comments