The article is in four parts, of which this is the first, whose contents are defined as follows:
The use of the contract metaphor in software engineering is a growing, but not entirely well understood, phenomenon. A software contract itself is, as in life, merely the agreement (explicit or otherwise) between the parties involved. It "defines a set of expectations between the two parties, that vary in strength, latitude and negotiability, and specified penalties [for contract violation]" [4]. The software contract metaphor encompasses not only functional behaviour—types, interfaces, parameters and return values, and so on - but also operational behaviour—complexity, speed, use of resources, and so on.
The issue of what action is to be taken in response to contract violation is a separate matter, just as in life. In this four-part article I'm focusing on the use of programmatic constructs—enforcements—that police the functional contracts codified in software: known as Contract Enforcement. Other aspects of the software contract metaphor are outside the scope of this discussion.
Just in the same way that human language contains redundancies and error-checking mechanisms, so we must ensure that our code does the same. You tell the compiler what your design is as you go, and it ensures that each time it picks up a crumb, it verifies the design.
Essentially, contract programming is about specifying the design, in terms of the behaviour, of your components (functions and classes), and asserting truths about the design of your code in the form of runtime tests placed within it. These assertions of truth will be tested as the thread of execution passes through the parts of your components, and will �fire� if they don�t hold. (Note: not all parts of contracts are amenable to codification in current languages, and there is some debate as to whether they may ever be [4]. This does not detract from the worth of contract programming, but it does define limits to its active realisation in code. In this article, I will be focusing on the practical benefits of codifying contract programming constructs.)
The behaviour is specified in terms of function/method preconditions, function/method postconditions, and class invariants. (There are some subtle variations on this, such as process invariants, but they all share the same basic concepts with these three elements.) Preconditions state what conditions must be true in order for the function/method to perform according to its design. Satisfying preconditions is the responsibility of the caller. Postconditions say what conditions will exist after the function/method has performed according to its design. Satisfying postconditions is the responsibility of the callee. Class invariants state what conditions hold true for the class to be in a state in which it can perform according to its design; an invariant is a �consistency property that every instance of the class must satisfy whenever it�s observable from the outside" [4]. Class invariants should be verified after construction, before destruction, and before and after the call of every public member function.
Let�s begin with a look at a simple function, strcpy()
, which is implemented along the lines of:
char *strcpy(char *dest, char const *src) { char *const r = dest; for(;; ++dest, ++src) { if(�\0� == (*dest = *src)) { break; } } return r; }What are its pre-/post-conditions? Let
N
be the number of characters pointed to by
src
that do
not contain the null-terminating character
�\0� (0)
.
Some preconditions are:
src
points to a sequence of N + 1 characters (type char
) each of whose value is accessible by the expression *(src
+ n), where n is an integer in the range [0, N + 1)dest
points to a block of memory that is writable for a length of N + 1 (or more) characterssrc
+ n) == *(dest
+ n) holds truedest
parameterchar *strcpy(char *dest, char const *src) { char * r; /* Precondition checks */ assert(IsValidReadableString(src)); /* Is src valid? */ assert(IsValidWriteableMemory(dest, 1 + strlen(src))); /* Is dest valid? */ for(r = dest;; ++dest, ++src) { if(�\0� == (*dest = *src)) { break; } } return r; }where:
assert()
is the standard C macro that calls abort()
if its expression evaluates false (0)
IsValidReadableString()
is a notional system call that tests to see if a pointer refers to a null-terminated string whose contents span read-accessible memorystrlen()
is the standard C function that returns the number of characters in a null-terminated stringIsValidWriteableMemory()
is a notional system call that tests that a pointer refers to a certain size of writeable memory.When a contract enforcement test, such as the assert()
s in the example, fails, it is said to fire, and the code is said to have violated its contract, and be in a violated state or an invalid state. By definition, the firing of a contract violation within a component is a message from that code�s author(s) that states precisely and absolutely that the component has violated its design, and no future expectations about its behaviour can be made, and no guarantees given. As Christopher Diggins points out [5], �design flaws are transitive. There is no known method in software engineering able to predict that a detected design flaw in a particular area has not corrupted the design of the rest of the software including the other assertions potentially causing them to falsely accept incorrect contracts". I will examine this issue in some rigour in part 2, with respect to precondition violations, and their potential recoverability.
Any methodology that makes use of software contracts and enforces their conditions via runtime tests needs to address three important elements of the enforcement: Detection, Reporting and Response. When using assert()
these three are effectively carried out in one place: Detection comprises a conditional test on the given expression; Reporting involves a call to fprintf()
or equivalent to display representative information, usually comprising file name + line number, the failed expression, and possibly some additional qualifying information (see Chapter 1 of Imperfect C++ [6]); Response is termination via a call to exit()
or abort()
. It�s important to realise that, in the examples in this part, use of assert()
is an implementation detail, just one means of effecting the enforcement, and quite peripheral to the the contract itself. Contracts may be enforced in other ways, e.g. via exceptions, which we�ll look at in Part 4.
#if defined(ACMELIB_TEST_POSTCONDITIONS) static char *strcpy_impl(char *dest, char const *src); char *strcpy(char *dest, char const *src) { char *const d = dest; char const *const s = src; char *r; /* Precondition checks */ assert(IsValidReadableString(src)); assert(IsValidWriteableMemory(dest, 1 + strlen(src))); /* Call 'actual' function */ r = strcpy_impl(dest, src); /* Postcondition checks. */ assert(0 == strcmp(d, s)); /* Are all contents the same? */ assert(r == d); /* Has it returned the right destination? */ return r; } static char *strcpy_impl(char *dest, char const *src) #else /* ? ACMELIB_TEST_POSTCONDITIONS */ char *strcpy(char *dest, char const *src) #endif /* ACMELIB_TEST_POSTCONDITIONS */ { . . . // Same impl as shown previously for strcpy() }The reason for the separation into inner and outer functions is that the tests need to be outside the (inner) function context, in order to that the author of the tests can be confident that he/she is seeing the true post-condition. This is especially important in C++ where the destructors of local scope objects might affect the post-conditions after their ostensibly �final� test.
In practice, one tends to leave postcondition testing in the too-hard basket, save for exceptional cases where the benefits outweigh the hassles. (One such hassle in this case would be ensuring that strlen()
doesn�t call strcpy()
, otherwise we may have a little stack problem.) Note that, in principle, there is almost no difference between checking the post-condition of a function in a wrapper function as shown above or in the function�s actual client code. It�s just that the former case is only done (once) by the library writer, who�s the one who should do it, and the latter is by the library user, who may be ignorant of the full behavioural spectrum and/or out of date with respect to changes in the valid behaviour since they wrote their tests.
Dollar
class:
class Dollar { public: explicit Dollar(int dollars, int cents) : m_dollars(dollars) , m_cents(cents) {} public: Dollar &add(Dollar const &rhs); Dollar &add(int dollars, int cents); public: int getDollars() const; // returns # of dollars int getCents() const; // returns # of cents int getAsCents() const; // returns total amount in cents private: int m_dollars; int m_cents; };Given this very simple class, what can we say and do about its invariant? Well, since one can, in principle, have or owe any amount of money, we can say that the valid range of dollars is anything that can be stored in an
int
. (If you�ve more than $2B, you might opt for a
long long
.) However, dollars, whether Australian, Canadian, or for any other country that has them, has only ever 100 cents per dollar. Thus we can say that the invariant for our
Dollar
class is that cents must not be more than 99. Hence we might write our invariant in the
private
member function
is_valid()
:
class Dollar { . . . private: bool is_valid() const { if( m_cents < 0 || m_cents > 99) { return false; } return true; } . . . };Note that this assumes that the cents field is always positive, and that negative amounts are represented in the sign of
m_dollars
only, e.g. $-24.99 would be represented as
m_dollars
= -24,
m_cents
= 99. If we chose to represent negativity in the total amount in both members, our invariant would need to reflect that also. Were we to do that, we�d also be able to state more in our invariant about the relationship between negative values in the member variables:
bool Dollar::is_valid() const { if(::abs(m_cents) > 99) { return false; } if((m_cents < 0) != (m_dollars < 0)) { return false; } return true; }Let�s look at how we hook in the invariant:
Dollar & Dollar::add(int dollars, int cents) { assert(is_valid()); // Verify invariant on method entry // . . . code to add the two amounts . . . assert(is_valid()); // Verify invariant on method exit return *this; }Note that we show a strategy for asserting on calls to invariants shown here, rather than having the invariant function itself fire the assertions. With complex classes it is also common to see some reporting occur within the invariant function, while the assertion is applied on the return value. For further discussions on this subject see Chapter 1 of Imperfect C++ [ 7]. The
is_valid()
method and its tests define and enforce the criteria for the
Dollar
's representational contract: it's a representation invariant. Simply: if
is_valid()
returns false, then there's either a design error in Dollar, or it has been corrupted (either by an undetected pre-condition violation, or by some other part of the processing tramping on its memory). An alternative view of specifying invariants is the public invariant. An example for
Dollar
would be:
"For anyDollar
instance d, either the expressiond.getDollars() + d.getCents() == d.getAsCents() && g.getCents() < 100
holds true ifd.getDollars()
returns a non-negative value, otherwise the expressiond.getDollars() - d.getCents() == d.getAsCents() && g.getCents() < 100
holds true."
Such public invariants do not lend themselves to association with the class implementation (i.e. as methods) as readily as representational invariants because it's customary for public methods to check invariants. If the invariant is comprised of public methods, this would lead to (possibly complex) additional logic to avoid experiencing recursive calls. For that reason they're not considered further in this article.
There�s general agreement that contract programming is a good thing, and in fact most of us have been doing some form of contract programming for years, as we�ve been using assertions to enforce assumptions about the design of our code. What contract programming delivers is a methodology for the unequivocal identification of violation of software design, by using constructs built into it by its author(s), who are the only ones qualified to make such a determination. And, to a significant degree, it provides in code what previously was commonly only expressed through documentation. This gives significant rewards in terms of code quality. I�ll discuss practical examples of this in parts 2 and 4. For now, let�s consider the theoretical perspective, looking at postconditions.
A thorough postcondition is an equivalent description of the algorithm used in a function, but written in an orthogonal manner. For example, if you have a sort function, a check on its postcondition would verify that the input array is indeed sorted according to the function�s intent [8]. Now, let's assume that there's a 90% chance that the sort algorithm is implemented correctly, and a 90% chance that the postcondition was written correctly. If the specifications are orthogonal, the probability of both failing on the same data is 10% times 10%, or 1%. So by writing twice as much code, we may achieve 10 times the reliability. That's the real magic of contract programming. It's the same idea behind, in aircraft design, having independent boxes controlling a critical task. The controllers have different electronics, different CPUs, different algorithms, and the software is written by different teams. They vote on the answer, and the system automatically shuts down if they disagree. This is how very high reliability is achieved even though each controller itself is nowhere near highly reliable.
The discussion thus far pretty much covers basic contract programming concepts, and is amply sufficient to serve as a base for the discussions I want to present in this article. It is, however, just scratching the surface of contract programming. For example, class invariants get inherited, as do post and pre conditions. This is one aspect of contract programming that is particularly difficult to emulate in C++. (Note: the Digital Mars C/C++ compiler has had support for contract programming as a language extension since 2000 [9].) Further, the complexity of callouts—a public method of class B calling a public method of class A as part of the modification of internal state, which in turn invokes another class B method which erroneously fires the invariant—is not considered further at this time.
If you want further convincing of the goodness of Contract Programming, there are several important works, including [1, 7, 10]. For now, we�re going to take as read that that�s accepted. Where the real controversy resides, however, is on what one can and should do in response to contract violations. That is the main subject of this article, and will occupy much of parts 2-4.
Ok, ok. What this preposterous initialism—only Mongolian throat singers could make it into an acronym!—actually represents is Absence Of Evidence Of Error Is Not Evidence Of Absence Of Error. In propositional logic terms, error detection/code verification is an implication, not a bi-implication. In other words, if an error is detected in your code, your code can be said to be erroneous, but if no error is detected in your code, it cannot be inferred that your code is correct. We�ll see the ramifications of this throughout the article. I should point out that this is a must-buy-in principle—if you don�t accept it you�ll be wasting your time in reading on.
From a practical perspective, some operating systems can determine some memory corruptions, such as accessing an unmapped page, or attempting to write to a read-only page, or what not. But in general there's no overarching help from hardware or anything else to be had for compiled languages. And even in such cases as there are, a programmer may choose to rely on an operating system catching an access violation to effect expected program behaviour—this is the way that some operating systems provide dynamic stack allocation (see Chapter 32 of Imperfect C++ [7]).
There are two main reasons why this must be so. First, the Heisenbergian: How can we measure something if we�re modifying it? If contract-programming constructs were to be part of the normal function logic, then they�d need to be subject to quality assurance, requiring contract enforcements on the contract enforcements. And so on, and so on, ad absurdum. Second, the practical: We need to be able to remove some/all contract enforcements for reasons of efficiency, and/or to change the checking/reporting mechanisms and responses, depending on application context. (Note: the removal of a particular enforcement does not affect the contracted behaviour it was policing. The removal of all enforcements does not mean that the associated contracts have been removed, nor does the absence of the enforcements imply that adherence to the contracts' provisions is no longer mandatory.)
This�ll be bread and butter to anyone who�s used asserts, since you will know that the cardinal rule is not to effect changes in the clauses of assertions, because it can lead to the worst of all possible debugging scenarios: where the code works correctly in debug mode and not in release mode. As soon as the line blurs between normal code and contract enforcement code, things are going to get squiffy, and your clients are going to be displeased.
Unfortunately, this tends to foster a blurring in the intended purposes of contracts and exceptions in the minds of engineers. Historically, this has tended to be much less the case in C and C++, because most C/C++ developers have used assertions for contract enforcement. Assertions in C and C++ tend to be included in debug builds only, and elided in release builds for performance reasons. However, as the programming community appreciation for contract programming grows, there is increasing interest in using contract enforcement in release builds. Indeed, as we�ll see in part 4 of this article, there is a very good argument for leaving contracts in released software, since the alternative may be worse, sometimes catastrophically so. Thus, there exists a very real danger that the same misapprehension may enter the C++ community psyche, so it�s worth discussing the issues here.
A thrown exception typically represents an exceptional condition that a valid program may encounter and, perhaps, recover from, such as inability to open a socket, or access a file, or connect to a database. A process in which such an exception is thrown remains a valid process, irrespective of whether it might continue to execute and attempt to reopen/access/connect, or whether it emits an error message and shuts down gracefully. Further, exceptions may also be used as a part of the processing logic for a given algorithm/component/API, although this is less often the case, and tends to be frowned on in the main [7, 13, 14].
There are other kinds of exceptions, from which a process cannot generally recover, but which do not represent an invalid state. We may call these Practically Irrecoverable Exceptional Conditions. The most obvious example is an out-of-memory condition. In receipt of such an exception, a process can often do little else but close down, though it is still in a valid state. It�s exactly analogous to not being able to open a file, but for the fact that the reporting and response mechanisms are likely to want to use memory; in such cases you should consider using memory parachutes [15], which can work well under some circumstances. Another practically unrecoverable exceptional condition is failure to allocate Thread-Specific Storage [7, 16, 17] (TSS) resources. To use TSS one needs to allocate slots, the keys for which are well-known values shared between all threads that act as indexes into tables of thread- specific data. One gets at one's TSS data by specifying the key, and the TSS library works out the slot for the calling thread, and gets/sets the value for that slot for you. TSS underpins multi-threaded libraries - e.g. errno / GetLastError()
per thread is one of the more simple uses - and running out of TSS keys is a catastrophic event. If you run out before you've built the runtime structures for your C runtime library, there's really no hope of doing anything useful, and precious little you can say about it.
In contrast to the activity of exceptions in indicating exceptional runtime conditions or as part of program logic, a contract violation represents the detection of a program state that is explicitly counter to its design. As we�ll see in The Principle Of Irrecoverability (in part 2), upon a contract violation the program is, literally, in an invalid state, and, in principle, there is no further valid action that the program may perform. Though nothing deleterious whatsoever might happen, the program has theoretically limitless potential for doing harm. In practice this tends to be limited to the current process, or the current operating environment, but could in theory be as bad as sending a proclamation of eternal war to Alpha Centuri!
Let's say we have a program that gets a date from its input, perhaps typed in by a user. At the point where the input is accepted into the program as a date, it should be validated as being an acceptable date, i.e. dates like 37-July-2004 should be rejected with a suitable message and retry presented to the user. Once a date has been validated and accepted into the logic of the program, however, at some level the internal architecture of the program will always assume valid dates. (To not do this, but instead to codify invalid-date branches at every level of the application, would be both very inefficient and lead to extremely complex code.)
At the point at which that assumption is made, a function precondition should reflect that assumption, and it would be appropriate to place a contract enforcement (e.g. an assert(IsDateValid(theDate));
or assert(theDate.is_valid());
) to guard that assumption. This gives confidence to the application programmer that at that point it really is a valid date, that nothing bad slipped by the user input validator, and that nothing else corrupted it in the meantime. A message to retry to the user at this point would make no sense at all. An invalid date at that point does not represent bad input; it represents a bug in the program. Thus contracts are designed to find bugs in the program, not bad data being input to the program.
Unfortunately, there tends to be ambiguities in the use of exceptions in some languages/libraries, which can lead to confusion over what falls within the purview of contract programming. In Java, exceptions will be thrown upon array overflow errors. This exception is part of the specification of Java. So, the following code is legitimate Java:
try { int[] array = new int[10]; int i; for(i = 0; ; ++i) {} } catch(ArrayIndexOutOfBoundsException e) {}Thus we see the overflow exception being used as part of the normal control flow logic. Some Java programmers use this practice, meaning that array overflows in Java are not useable for contract programming purposes, because that would violate the Principle of Removability.
The same situation exists with regards to the at()
member of C++�s std::basic_string
, and appears to cause quite a degree of misunderstanding, generally in confusion between whether operator []()
and at()
are equivalent. Let�s have a look at at()
�s signature:
const_reference at(size_type index) const; reference at(size_type index);The C++ standard [ 18] states the following:
Requires:index <= size()
Throws:out_of_range
, ifindex >= size()
Returns:operator [](index)
From a contract programming perspective, this is a little misleading, since the Requires part inclines one to think that the contract stipulates that index <= size()
. This is not so. Indeed the precondition for at()
is empty, i.e. the sum of all possible values for index
[19]:
at()
�s
Precondition: (empty)
The postcondition is where the interest resides, since it states:
In other words, ifat()
�s Postcondition: returns reference to theindex
�th element ifindex < size()
, otherwise throwsout_of_range
.
index
is within range it returns a reference to the corresponding element, otherwise it throws an exception. All that�s none too surprising. Now consider how this differs from the
operator []()
method(s):
const_reference operator [](size_type index) const; reference operator [](size_type index);The standard [ 18] states:
Returns: IfThis is a thoroughly different kettle of fish. If we request a valid index we get back a reference to the corresponding element, just as withindex < size()
returnsdata()[index]
. Otherwise, ifindex == size()
, theconst
version returnscharT()
. Otherwise, the behaviour is undefined.
at()
. (Note that the
const
version defines the valid range to be
[0, size() + 1)
, whereas in the non-
const
case it is
[0, size()
). Go figure! [
20]) However, if we do not get the index right, the behaviour is
undefined.
Here's a simple rule for anyone that's confused about what the language dictates: The C++ standard [18] does not address the issue of contract enforcement at all, although it does do an acceptable job of the description of the contracted behaviour (ambiguous language such as basic_string
�s at()
�s "Requires" notwithstanding).
at()
and
operator []()
. The contract for the mutable (non-
const
) version of
operator []()
is as follows:
Other than confused users and an over-baked standard string class [ 23], what are the ramifications of these differences? Simply, it means that different element access paradigms are supported. The normal manipulation of arrays in C and C++, via knowing the range, is supported byoperator []()
�s Precondition:index < size()
operator []()
�s Postcondition: returns reference to theindex
�th element.
operator []()
:
int main() { std::string s(�Something or other"); for(std::string::size_type i = 0; i < s.size(); ++i) { std::cout << i << ": " << s[i] << std::endl; } return 0; }And the catch-out-of-bounds method, as shown in the Java example earlier, is supported by
at()
:
int main() { std::string s(�Something or other"); try { for(std::string::size_type i = 0; ; ++i) { std::cout << i << ": " << s.at(i) << std::endl; } } catch(std::out_of_range &) {} // Do nothing return 0; }It is important to realise that these both represent entirely valid programs, in which the client code respects the contracts of the respective
std::basic_string
methods used. To reiterate, specifying an out-of-bounds index for
at()
is
not a contract violation, whereas it most certainly is for
operator []()
. This delineation between exception-throwing and undefined behaviour (i.e. contract violations) exists equally outside the standard. Consider the STL mapping for the Open-RJ library [24]. The
record
classes provides an
operator [](char_type const *fieldName)
, which throws a
std::out_of_range
if
fieldName
does not corresponding to an existing field for that record within the database. Now it's certainly not the case that asking for a field (by name) that does not exist is invalid code. It affords a simple and elegant style in client code:
openrj::stl::file_database db("pets.orj", openrj::ELIDE_BLANK_RECORDS); try { for(openrj::stl::database::iterator b = db.begin(); . . . ) // enumerating db's records { openrj::stl::record r(*b); std::cout << "Pet: name: " << r["Name"] << "; species: " << r["Species"] << "; weight: " << r["Weight"] << std::endl; } catch(std::out_of_range &) { std::cout << "One of the records did not have the Name, Species and Weight fields" << std::endl; }The
record
class also provides an
operator [](size_type index)
method, for which an out of bounds index represents a contract violation. Thus, the following code is a badly formed program:
. . . openrj::cpp::Record record(*b); for(size_t i = 0; ; ++i) { std::cout << record[i].name << ": " << record[i].value << std::endl; } . . .Whereas the former is perfectly valid code, and is a reasonable tool for checking the validity of Pets databases—using
Record::operator []()
�s thrown exception in the case that a record does not contain a field of the given name—the latter is ill-formed, and is going to cause you grief (i.e. a crash).
And if you�re still sceptical whether exceptions may be part of a function�s contract, consider the case of the operator new()
function. If throwing an instance of bad_alloc
(or something derived from bad_alloc
) were not within its contract, it would mean that memory exhaustion—a runtime condition largely outside the control of the program designer—would be a contract violation, that is to say an unequivocal statement of design contradiction! Now that�d make writing good software something of a challenge ...
Thanks also to the members of the D newsgroup (news://news.digitalmars.com/d) for a similarly stimulating discussion in April 2005, particularly Ben Hinkle, Derek Parnell, George Wrede, and Regan Heath. You made me work very hard to fill in the gaps in the Principle of Irrecoverability that had previously only been held together by instinct and crossed fingers. Special thanks to Sean Kelly for stimulating the thought process that led to The Fallacy of the Recoverable Precondition Violation (part 2).
Thanks also to the following reviewers: Andrew Hunt, Bjorn Karlsson, Christopher Diggins, Kevlin Henney, Nevin Liber, Sean Kelly, Thorsten Ottosen, Walter Bright. Special thanks to Chris, whose dryness and rigour in review has proven such a valuable compliment to my intuition and verbosity, and to Kevlin, whose eloquent criticism would gently give pause to the most doubtless evangelist. And I�d also like to thank my editor Chuck Allison, for actions above and beyond the call of duty in helping me prepare this leviathan meal into digestible servings.
Despite all help received, any errors, bad jokes and poor judgements are my own.
Thank you for reading,
Matthew Wilson
true
, rather than the empty condition. This would nicely balance the theoretical condition false
for a function that had no satisfiable precondition. We�ve all come across a couple of those in our travels � ;)const
) reference to the null-terminator is harmless, whereas returning a mutable (non-const
) reference is anything but. Whether this inconsistency is worth the modest increase in non-mutable flexibility is a debate outside the scope of this article.Have an opinion? Readers have already posted 18 comments about this article. Why not add yours?
Matthew Wilson is a software development consultant, contributing editor for C/C++ User's Journal, and creator of the STLSoft libraries (http://stlsoft.org/) . He is author of Imperfect C++ (Addison-Wesley, 2004), and is currently working on his next two books, one of which is not about C++. Matthew's appetite for coding challenges is matched only by his appetite for chocolate; he keeps them in check by writing articles and riding his bike (not always at the same time). He can be contacted via http://imperfectcplusplus.com/.
Artima provides consulting and training services to help you make the most of Scala, reactive
and functional programming, enterprise systems, big data, and testing.