Author: | Thorsten Ottosen |
---|---|
Contact: | nesotto@cs.auc.dk |
Organization: | Department of Computer Science, Aalborg University, and Dezide Aps |
Date: | 10th of September 2004 |
Number: | WG21/N1669 and J16/04-0109. This proposal is a revision of paper n1613. |
Working Group: | Evolution |
Copyright: | Thorsten Ottosen 2004. All rights reserved |
Abstract
Contract Programming 1 is about providing the programmer with stronger tools for expressing correctness arguments directly in the source code. This proposal describes a small assertion sub-language that unifies compile-time and run-time assertions. The benefits are among others a higher degree of self-documenting source code, better design tools, and easier debugging and testing.
Language support for Contract Programming has several benefits:
Please refer to n1613 for an extensive discussion of these benefits.
Already with the current proposal, it is quite remarkable how much of an interface that can be documented. For a good example, please see how std::vector is declared in vector_example.hpp; there is only a small portion of the specification of std::vector that cannot be described in the contracts.
The idea is to extend C++
These new contract scopes can contain assertions that may be evaluated either at compiler-time or at run-time as part of the program to verify their correctness.
The basic building blocks of contracts are assertions. Assertions come in two categories:
There are two flavors of static assertions:
default assertions
- syntax:
static constant-expression : string-literal ;
- example:
static is_pod<T>::value : "T must model a POD";
select assertions
- syntax:
- if( constant-expression ) static-assertion
- if( constant-expression ) static-assertion else static-assertion
- example:
- if( is_pod<T>::value )
static sizeof( T ) <= sizeof( long ) : "Only PODs smaller than long supported";
The static assertions are exactly like the ones described in n1617. The main difference is that we
There are three flavors of runtime assertions:
default assertions
- syntax:
boolean-expression ;
- example:
std::cout.good();
compound assertions
- syntax:
boolean-expression : expression ;
- example:
std::cout.good() : terminate();
select assertions
- syntax:
- if( condition ) runtime-assertion
- if( condition ) runtime-assertion else runtime-assertion
- example:
- if( empty() )
result == end();
We shall call the left hand side of a compound assertion the condition and the right hand side the action. The following general points are worth noting:
The select assertions remove the need for the implication operator that was discussed in n1613.
The syntactic freedom have been kept minimal to ensure a good documentation effect (see also Allow more statements in contracts).
Every visible variable or parameter is considered const within the scope of contracts (even mutable variables).
Every function call within the assertions must be either
- a const member function with no non-const reference or pointer arguments
- a free-standing function with no non-const reference or pointer arguments
- if the return value is a non-const reference or pointer, it must not be used in any context that would not allow a const qualified reference or pointer
(see also Allow mutable functions in contracts.)
Function calls within assertions do not check assertions; this is to prevent infinite recursion and to provide reasonable performance (as seen in the factorial_example.hpp).
Throwing an exception is sometimes convenient; bounds-checking is a good example (see also how it is done in vector_example.hpp ).
If the compiler can determine that the condition of an assertion is true, it may choose not to evaluate that assertion.
If the compiler can determine that the condition of an assertion is false, it may choose to call the broken contract handler or call the action without evaluating that condition.
The last rule opens up the door for other types of contracts where we can use the comma-operator as seen in the pointer_container_replace_example.hpp.
It is very useful to be able to take a copy of a variable to compare it with other values later. In particular, this is true in function postconditions and in loop invariants. These two contract scopes are also the only scopes where std::old() is allowed to appear.
The function can be said to have the following pseudo-declaration:
namespace std { template< class T > const T& old( const T& r ) precondition { static is_copy_constructible<T>::value : "old() requires the type to be Copy Constructible"; } }
The function must return a reference to a temporary variable. Assume i is a visible int, then the compiler translates
{ std::old( i ) == 42; }
into
{ const int __old_i( i ); __old_i == 42; }
A set of rules govern how std::old() behaves within a certain contract:
if it is applied to the same expression n times, that expression is only evaluated once and there only exists one copy of the expression,
the copy of its argument is taken
- after the precondition is evaluated if std::old() appears in function postconditions,
- before each loop iteration if std::old() appears in loop invariants.
The precondition should be evaluated first to allow it to report its errors first. Notice that we can only apply std::old() to objects of a type that defines a copy-constructor taking a const reference (ie., std::auto_ptr cannot be used).
The function declaration and definition are changed into:
- function-declaration1 : function-head
- function-declaration2 : function-head function-contract
- function-definition1 : function-declaration1 { function-body }
- function-definition2 : function-declaration2 do { function-body }
- function-contract : precondition-contractopt postcondition-contractopt
- precondition-contract : precondition { assertion-sequence }
- postcondition-contract : postcondition return-value-declarationopt { assertion-sequence }
- return-value-declaration: ( identifier )
example:
double sqrt( double r ) precondition { r > 0.; } postcondition( result ) { equal_within_precision( result * result, r ); }
In the return-value-declaration we declare a local variable that acts as a const reference to the computed return value. It follows that a return-value-declaration is illegal if the return type is void.
The following general rules apply:
If a function has both a declaration and a definition, the contracts must appear on the declaration.
The execution of a function is now
- evaluate the precondition assertion by assertion in order starting from the top,
- evaluate std::old() occurrences in the postcondition,
- evaluate the function body,
- evaluate the postcondition assertion by assertion in order starting from the top.
The execution of a member function is now as in 2, but
2.a is preceded by a call to the class invariant,
2.d is preceded by a call to the class invariant,
- rationale: the contracts might call public functions in the class which require the invariant to be established.
(See also when the class invariant is disabled.)
If the function F is virtual, we must require that
Section 3.5 of n1613 explains how little redefinition of preconditions is used. Even though subcontracting is theoretically sound, it ends up being useless in practice since the user cannot take advantage of the weaker precondition by performing a downcast 4. Also, the derived class can provide a separate function with a weaker precondition or the precondition can be specified via a virtual function.
Postconditions can be made stronger in a derived class as seen in the subcontracting_example.hpp. We can summarize the remaining rules for virtual functions as
The last rule ensures that the function in the derived class has a stronger postcondition by design. A programmer can via a downcast obtain the stronger guarantee, but the runtime check must still follow rule 4 above.
Constructors behaves much like member functions. This means that a constructor
The differences are that a constructor:
Destructors cannot have preconditions and postconditions. The class invariant, however, is executed before the destructor body. Notice that the rules for evaluating the class invariant ensures that exceptions thrown during evaluation of the class invariant cannot escape.
Within class scope it is possible to define a class invariant:
- syntax:
- invariant { assertion-sequence }
- example:
- see vector_example.hpp
The class invariant has the following properties:
It must appear in the declaration of a class.
It is inherited in the sense that the invariant of a derived class implicitly will have the super-class invariant ANDed to it (see also item 6-7).
example:
struct base { invariant { ... } }; struct derived : base { invariant { base::invariant(); // implicitly generated ... } };(Remark: the above is pseudo-code and is not affected by the disabling of assertions within assertions.)
It can always be called from within or outside the class as if it was a member function with the declaration void invariant() const;.
It is called implicitly from public
- member functions that exit abnormally via an exception 5
It is always called as if surrounded by a try-catch block:
try { class_invariant(); } catch( ... ) { std::class_invariant_broken(); }
- rationale: this ensures that exceptions thrown in the invariant cannot escape from functions and in particular not from the destructor. 6
For its execution during construction 7 holds that:
each assertion is executed in order starting from the top excluding any part inherited,
- rationale: if we do not exclude inherited parts, those parts would be evaluated multiple times.
For its execution after construction holds that:
- each assertion is executed in order starting from the top including any part inherited,
- if the class has any sub-classes, then the inherited portion is executed first,
- if the class has several sub-classes the order of the classes in the base-clause is followed.
If the body of a public function F1 calls another public member function F2 of the same instance, the invariant is not called implicitly during that call. 8
Section 7.4 of n1613 discusses the last issue in more detail. In general it seems reasonable to enforce invariants across class boundaries, but to allow a class some more latitude internally.
This revision introduces loop invariants. Loop invariants make it easier to verify correct loop behavior. The syntax is
loop-invariant: invariant { assertion-sequence }
An example can be found in factorial_example.hpp. The special function std::old() has a particular meaning inside a loop invariant. Assume i is a visible int; then when the compiler sees
invariant { std::old( i ) == i + 2; }
it is translated into something like
invariant { const int __old_i = i; // this executes before each loop iteration __old_i == i + 2; // this executes after each loop iteration }
To summarize the rules, we have
std::old() takes its copy before each loop iteration,
the assertions are evaluated after each loop iteration in order starting from the top,
the entire loop-invariant is evaluated as if it was surrounded by a try-catch block:
try { loop_invariant(); } catch( ... ) { std::loop_invariant_broken(); }
variables that have a greater scope than the loop are visible in the loop-invariant and std::old() can be applied to them,
loop-variables declared in the loop-body are visible in the loop-invariant as well and std::old() can be applied to them. 9
Notice that std::old() is used to specify variant behavior within the loop invariant---something that usually requires another keyword.
syntax:
for ( for-init-statement ; conditionopt ; expressionopt ) statement loop-invariantoptexample:
for( int i = 0; i != 10; ++i ) { std::cout << i; } invariant { i >= 1 && i <= 10; i == std::old( i ) + 1; }
syntax:
while ( condition ) statement loop-invariantoptexample:
int i = 0; while( i != 10 ) { std::cout << i; ++i; } invariant { i >= 1 && i <= 10; i == std::old( i ) + 1; }
syntax:
do statement loop-invariantopt while ( expression ) ;example:
int i = 0; do { std::cout << i; ++i; } invariant { i >= 1 && i <= 10; i == std::old( i ) + 1; } while( i != 10 );
To better support global state or namespace state this revision introduces namespace invariants:
- syntax:
- namespace-invariant: invariant { assertion-sequence }
example:
namespace foo { int buffer_size; int* buffer; invariant { static sizeof( int ) >= 4 : "int must be 32 bit"; buffer_size > 0; buffer != 0; } }
The properties of namespace invariants are:
they can appear multiple times in the same namespace across multiple translation units,
for each translation unit, the present invariants are executed once after the static initialization phase, that is, conceptually as the last part of the static initialization phase,
they are always called as if surrounded by a try-catch block:
try { namespace_invariant(); } catch( ... ) { std::namespace_invariant_broken(); }
the order of evaluation of the invariants is unspecified,
the order of evaluation of the assertions in an invariant is from top to bottom,
for any namespace N, N::invariant() executes all invariants from namespace N.
The default behavior for all default assertions is to call terminate() via a call to their respective handler. As seen below, we have a handler for each type of contract. If the default behavior is undesirable, the standard library provides the following handlers:
namespace std { void precondition_broken(); void postcondition_broken(); void class_invariant_broken(); void loop_invariant_broken(); void namespace_invariant_broken(); typedef void (*broken_contract_handler)(); broken_contract_handler set_precondition_broken_handler( broken_contract_handler r ) throw(); broken_contract_handler set_postcondition_broken_handler( broken_contract_handler r ) throw(); broken_contract_handler set_class_invariant_broken_handler( broken_contract_handler r ) throw(); broken_contract_handler set_loop_invariant_broken_handler( broken_contract_handler r ) throw(); broken_contract_handler set_namespace_invariant_broken_handler( broken_contract_handler r ) throw(); }
This should provide plenty of room for customization. The precondition for all the set_XX_handler functions should be r != 0.
The only control structure that this proposal allows inside contracts is an if-statement. The questions are then
why not allow all forms of control structures?
example:
{ for( size_type i = 0; i != size(); ++i ) *this[i] == T(); }
why not just treat a contract block as any normal C++ block?
For the first question we can observe that
pros:
- one can avoid calling a function that embeds the control structure
cons:
- more complex specification
- a reduced documentation effect since the abstraction level is lowered
If we make a contract block a normal C++ block, we make problem 2 above worse. In addition we must ask
cons:
- how should the compiler know what constitutes a run-time assertion?
- how should the compiler know what constitutes a static assertion?
The obvious answer to the last question is to add a new special keyword as proposed in n1617.
The current proposal prohibits side-effects as much as possible. Otherwise we could end up with different run-time behavior depending on whether or not an assertion is executed. Because C++ knows about constness, C++ really shines in this regard compared to other languages.
So the question is
If we were to provide a little more latitude, there could be at least two different ways to reuse mutable:
make the whole block mutable:
void X::foo( int i ) precondition mutable { ... }make individual assertions mutable:
void X::foo( int i ) precondition { mutable foo( i ); }
There seems to be at least two solutions:
Forbid function-try-blocks and contracts on the same function.
Allow it as in this example:
void foo( int i ) precondition { i > 0; } try { // ... } catch( ... ) { }
In case of 2, then the question would be
Pavel Vozenilek asks why contracts cannot be templated. Examples could be
template< class T > class Foo { invariant<T*> // partial specialization of invariant { // ... } invariant // default invariant here { // ... } }; template< class T > void foo( T r ) precondition<double> { /* only allow full specialization for functions */ } precondition { /* default */ }
Currently a failure handler function does not allow one to
It might be desirable to change the functions to
namespace std { class assertion_context { contract_context( const char* where, const char* assertion ) throw(); ~contract_context() throw(); const char* context() const throw(); }; void precondition_broken( const assertion_context& cc ); // ... etc }
or even
namespace std { extern std::function< void ( const assertion_context& ) > precondition_broken; // ... etc }
This would hopefully ensure good error-messages.
It could be considered that any static function
void invariant();
and any member function
void X::invariant() const;
automatically defines a namespace invariant and a class invariant, respectively. This would have the following consequences:
pros:
- it saves a keyword,
cons:
- it could change the behavior of those programs that already defines an invariant,
- it would make the syntax and semantics of invariant contracts different from pre- and postcondition contracts.
It might also be worth considering a --no_invariant_keyword compiler option for backward compatibility instead.
added loop invariants
added namespace invariants
added static assertions
changed use of keywords:
before
now
in
precondition
out
postcondition
return
a local variable
implies
if
in
std::old()
simplified subcontracting: now preconditions cannot be weakened
This section describes some crazy ideas that might serve as a basis for further discussion.
For the purpose of static analysis either by the compiler or a special tool, it might be convenient to be able to indicate certain functions were invalid for a period of time.
For example, calling operator*() on an iterator could be invalid, yet we cannot explain that to the compiler.
Imagine a magic function std::invalid_operation():
iterator end() postcondition( result ) { // // expression style // std::invalid_operation( *result, ++result, result++ ); // // function style // std::invalid_operation( operator*(), operator++(), operator+() ); }
These operations would then be invalid until a new member function was called that did not have the same invalid operation. For example
iterator i = container.end(); --i; // now the operations becomes valid *i = a_value; // ok ++i; // ok
A compiler could choose to either
C++'s exception specifications are not used very much in practice. This is unfortunate since documenting what exceptions (if any) a function throws is important.
In other words, the exception specification seems to be an important part of a functions contract. This proposal already checks the basic guarantee of exception-safety (by checking invariants), and we can already check the strong guarantee in a post-condition:
postcondition { *this == std::old( *this ); }
But there seem two other important cases as well:
So what if we changed the rules for
iterator begin() throw();
to mean
? If that were the case, throw() would be used much more.
The second situation is when a function exits via an exception, but we still want to guarantee more than the basic guarantee. Imagine that we could do something like this:
postcondition { if( throws( std::exception ) ) { std::old( foo ) == foo; // this variable has not changed std::old( bar ) != bar; // but this one has } }
This might provide a way to make stronger assertions in the presence of exceptions.
Alf Steinbach suggests that preconditions should be checkable without calling the function:
void foo( ... ) precondition { ... } // ... if( foo.precondition( ... ) ) foo( ... ) else report_error();
Walter Bright has extensive experience from implementing Contract Programming in the C++ and D compilers from Digital Mars. He asserts that it will take about one man month to implement this proposal.
/////////////////////////////////////// // Tools /////////////////////////////////////// template< class Iter, class T > bool all_equals( Iter first, Iter last, const T& val ) { /* for simplicity, let us assume T's can be compared */ } template< class Iter, class Size > bool equal_distance( Iter first, Iter last, Size size ) { /* internal tagging mechnism so even input iterators can be passed */ } /////////////////////////////////////// // New vector interface /////////////////////////////////////// template< class T, class Alloc = allocator<T> > class vector { invariant { ( size() == 0 ) == empty(); size() == std::distance( begin(), end() ); size() == std::distance( rbegin(), rend() ); size() <= capacity(); capacity() <= max_size(); static is_assignable<T>::value : "value_type must be Assignable" ; static is_copy_constructible<T>::value : "value_type must be CopyConstructible" ; } public: typedef Alloc allocator_type; typedef typename Alloc::pointer pointer; typedef typename Alloc::const_pointer const_pointer; typedef typename Alloc::reference reference; typedef typename Alloc::const_reference const_reference; typedef typename Alloc::value_type value_type; typedef ... iterator; typedef ... const_iterator; typedef ... size_type; typedef ... difference_type; typedef reverse_iterator<iterator> reverse_iterator; typedef reverse_iterator<const_iterator> const_reverse_iterator; vector() postcondition { empty(); } explicit vector( const Alloc& al ) postcondition { empty(); al == get_allocator(); } explicit vector( size_type count ) postcondition { size() == count; all_equals( begin(), end(), T() ); } vector( size_type count, const T& val ) postcondition { size() == count; all_equals( begin(), end(), val ); } vector( size_type count, const T& val, const Alloc& al ) postcondition { size == count; all_equals( begin(), end(), val ); al == get_allocator(); }: vector( const vector& right ) postcondition { right == *this; } template< class InIt > vector( InIt first, InIt last ) precondition { static is_input_iterator<InIt>::value : "Iterator argument must model an InputIterator"; } postcondition { equal_distance( first, last, size() ); } template< class InIt > vector( InIt first, InIt last, const Alloc& al ); precondition { static is_input_iterator<InIt>::value : "Iterator argument must model an InputIterator"; } postcondition { equal_distance( first, last, size() ); al = get_allocator(); } void reserve( size_type count ) precondition { count < max_size() : throw length_error(); } postcondition { capacity() >= count; } size_type capacity() const; postcondition( result ) { result >= size(); } iterator begin() postcondition( result ) { if( empty() ) result == end(); } const_iterator begin() const postcondition( result ) { if( empty() ) result == end(); } iterator end(); const_iterator end() const; reverse_iterator rbegin() postcondition( result ) { if( empty() ) result == rend(); } const_reverse_iterator rbegin() const postcondition( result ) { if( empty() ) result == rend(); } reverse_iterator rend(); const_reverse_iterator rend() const; void resize( size_type newsize ) postcondition // version 1: with an if { size() == newsize; if( newsize > std::old(size()) ) all_equals( begin() + std::old(size()), end(), T() ); } void resize( size_type newsize, T val ) postcondition // version 2: with ternary operator { size() == newsize; newsize > std::old(size()) ? all_equals( begin() + std::old(size()), end(), val ) : true; } size_type size() const postcondition( result ) { result <= capacity(); } size_type max_size() const postcondition( result ) { result >= capacity(); } bool empty() const postcondition( result ) { result == ( size() == 0 ); } Alloc get_allocator() const; reference at( size_type off ); precondition { off < size() : throw out_of_range(); } const_reference at( size_type off ) const; precondition { off < size() : throw out_of_range(); } reference operator[]( size_type off ) precondition { off < size(); } const_reference operator[]( size_type off ) precondition { off < size(); } reference front(); precondition { not empty(); } const_reference front() const; precondition { not empty(); } reference back() precondition { not empty(); } const_reference back() const; precondition { not empty(); } void push_back( const T& val ) precondition { size() < max_size(); } postcondition { back() == val; size() == std::old(size()) + 1; capacity() <= std::old(capacity()); } void pop_back() precondition { not empty(); } postcondition { size() == std::old(size()) - 1; } template< class InIt > void assign( InIt first, InIt last ) precondition { static is_input_iterator<InIt>::value : "Iterator argument must model an InputIterator"; "The range [first,last) is not a sub-range of [begin(),end())"; } postcondition { equal_distance( first, last, size() ); } void assign( size_type count, const T& val ) precondition { count <= max_size(); } postcondition { all_equals( begin(), end(), val ); } iterator insert( iterator where, const T& val ) precondition { size() < max_size(); } postcondition( result ) { *result == val; size() == std::old(size()) + 1; capacity() >= std::old(capacity()); if( capacity() > std::old(capacity()) ) "All iterators have been invalidated"; else "All iterators in the range [where,end()) have been invalidated"; } void insert( iterator where, size_type count, const T& val ) precondition { size() + count <= max_size(); } postcondition { size() == std::old(size()) + count; capacity() >= std::old(capacity()); if( capacity() == std::old(capacity()) ) { all_equals( std::old(prior(where)), std::old(prior(where)) + count, val ); "All iterators in the range [where,end()) have been invalidated"; } else "All iterators have been invalidated"; } template< class InIt > void insert( iterator where, InIt first, InIt last ) precondition { static is_input_iterator<InIt>::value : "Iterator argument must model an InputIterator"; "The range [first,last) is not a sub-range of [begin(),end())"; size() + count <= max_size(); } postcondition { size() == std::old(size()) + count; capacity() >= std::old(capacity()); if( capacity() == std::old(capacity()) ) { all_equals( std::old(prior(where)), std::old(prior(where)) + count, val ); "All iterators in the range [where,end()) have been invalidated"; } else "All iterators have been invalidated"; } iterator erase( iterator where ); precondition { not empty(); where != end(); } postcondition( result ) { size() == std::old(size()) - 1; if( empty() ) result == end(); "All iterators in the range [where,end()) have been invalidated"; } iterator erase( iterator first, iterator last ) precondition { size() >= std::distance( first, last ); } postcondition( result ) { size() == std::old(size()) - std::distance( first, last ); if( empty() ) result == end(); "All iterators in the range [first,end()) have been invalidated"; } void clear(); postcondition { empty(); } void swap( vector& right ) postcondition { std::old( *this ) == right; std::old( right ) == *this; } }; // class 'vector'
// // This example shows how Contract Programming removes duplicate // knowledge because it can turn comments into code. // // It also shows a very interesting way of checking that // the function's postcondition holds: the implementation // uses iteration, but the postcondition uses recursion // to verify the return value. // // remark: there is no deep/infinite recursion because // contracts are not checked while evaluating // a contract. // /////////////////////////////////////////////////////////// // Ordinary form of documentation /////////////////////////////////////////////////////////// //! Factorial algorithm /*! \pre 'n >= 0' \post returns '1' if 'n == 0' returns 'factorial(n-1)*n' otherwise */ template< class T > inline T factorial( T n ) { BOOST_ASSERT( n >= 0 ); if( n == 0 ) return 1; T result = n; T old_result = result; for( --n; n != 0; --n ) { result *= n; BOOST_ASSERT( n > 0 ); BOOST_ASSERT( result > old_result && "integer has wrapped around" ); old_result = result; } return result; } /////////////////////////////////////////////////////////// // Contract Programming-style documentation /////////////////////////////////////////////////////////// template< class T > inline T factorial( T n ) precondition { n >= 0; } postcondition( result ) { if( n == 0 ) result == 1; else result == n * factorial(n-1); } do { if( n == 0 ) return 1; T result = n; for( --n; n != 0; --n ) result *= n; invariant { n >= 0; result > std::old( result ) && "integer has wrapped around"; } return result; }
// // This example shows how Contract Programming can // provide all the functionality of the 'restrict' // keyword. Moreover, it will work for iterators that // are not just pointers. // // Caveat: The iterators of eg. std::deque are random-access, but will // still work erroneously with this example. // namespace std { template< class RandomAccessIterator bool arrays_dont_overlap( RandomAccessIterator first, RandomAccessIterator last, RandomAccessIterator first2, RandomAccessIterator last2 ) { return std::less< typename iterator_value<T>::type* >( &*prior(last), &*first2 ) || std::less< typename iterator_value<T>::type* >( &*prior(last2), &*first ); } template< class RandomAccessIterator > void fast_copy( RandomAccessIterator first, RandomAccessIterator last, RandomAccessIterator out ) precondition { static is_pod< typename iterator_value<RandomAccessIterator>::type >::value : "Only PODs can be copied"; static is_random_access_iterator<RandomAccessIterator>::value : "The iterator must model a Random Access Iterator"; arrays_dont_overlap( first, last, out, out + std::distance(first,last) ); } do { memcpy( &*out, &*first, std::distance(first,last) ); } }
// // This example shows how Contract Programming // allows one to keep the virtual function public // and still enforce the contract of the function // class shape { public: virtual ~shape() {} virtual int compute_area() const = 0 postcondition( result ) { result > 0; } }; class circle : public shape { public: int radius() const; virtual int compute_area() const postcondition( result ) { result == pi * radius() * radius(); } };
// // This example shows how we can perform // a clean-up action as a side-effect in a precondition. // // Side-effects are normally banned from contracts, but in this // we do it to obhold exception-safety requirements. // template< class T > class ptr_vector { public: // ... void replace( T* p, size_type where ) precondition { p != 0 : throw bad_ptr(); where < size(); : delete p, throw bad_index(); } };
The table below contains a comparison of how Contract Programming is implemented in D, Eiffel and C++.
Feature | ISE Eiffel 5.4 | D | C++ Proposal |
keywords | require, ensure, do, require else, ensure then, invariant, old, result, variant | in, out, body, invariant, assert, static | precondition, postcondition, do, invariant, static |
on failure | throws exception | throws exception | defaults to terminate(), defaults can be customized, might throw |
expression copying in postconditions | yes, old keyword | no | yes, std::old() function |
subcontracting | yes | yes | yes, but only considers postconditions |
contracts on abstract functions | yes | no | yes |
arbitrary code contracts | yes | yes | no, must be const correct |
function code ordering | pre -> body -> post | pre -> post -> body | pre -> post -> body |
compile-time assertions | no | yes | yes |
loop invariants | yes | no | yes |
loop variants | yes | no | yes, std::old() inside loop invariants |
const-correct | no | no | yes |
invariant calls |
|
|
|
disabling of checks during assertions | yes | no | yes |
when public func. call public func. | disable all checks | disable nothing | disable invariant |
removable from object code | yes | yes | only default assertions |
Special thanks goes to
[1] | The original term "Design by Contract" is abandoned for good since it appears to have been trademarked in 2003. The term "Contract Programming" was suggested by Walter Bright. |
[2] | Even experienced programmers can choke on the word "invariant" and what it means for a class to have one. There are a few simple rules we want all programmers to learn:
|
[3] | This latitude allows programmers to turn off and on compound assertions using appropriate macros and can be helpful if the more levels of assertions are wanted. For example, an assertion like is_foo() : (void)0; can be discarded. |
[4] | Assume we want to take advantage of a weaker precondition. Normally that precondition is weaker by design, that is, the two preconditions are ORed no matter what they contain. Even if we perform a downcast, the runtime system must call first the original contract (which will then fail). Then the control is handled to the derived precondition which can then fail or not. This seems extremely messy and might require try-catch block around the first precondition. The alternative is to only check the derived precondition if we have performed a downcast. The consequence is that we might have a precondition that is in fact stronger than the original! In general it is probably also impossible to verify that a contract is weaker given that it can contain arbitrary expressions. This approach does not seem very attractive either. If we also take into consideration that good examples with weakened preconditions are extremely rare, it is be better to abandon the idea all together. Thanks goes to Scott Meyers and Attila Fehér for their input on this issue. |
[5] | To ensure that the function gives the basic guarantee of exception-safety. |
[6] | One can imagine an approach where exceptions could escape from an invariant by not checking invariants when functions exit via an exception. The benefits of such an approach are not obvious and it will complicate flow analysis unnecessarily. |
[7] | During construction means until the post-condition of the constructor has ended. |
[8] | An implementation would not be required to detect when control leaves a class in general but merely to disable invariants until the function returns. This means that if A::foo() calls B::foo() which calls A::bar(), then the invariant of A will not be checked on entry to or exit of A::bar(). However, B::foo() may choose to check the invariant of A manually. |
[9] | This is contrary to the loop condition which cannot see these variables. |