The Artima Developer Community
Sponsored Link

The C++ Source
Contract Programming 101
The Nuclear Reactor and the Deep Space Probe, Part I
by Matthew Wilson
January 1, 2006

<<  Page 2 of 4  >>

Advertisement

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.

Postconditions

As I discuss in Chapter 1 of Imperfect C++ [6], implementing postconditions in C++ is a non-trivial undertaking in general, and relies on some hoop-jumping. In this case, it would look like the following:
#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.

Class Invariants

That just leaves invariant testing. Just as we�ve seen with postconditions, we can�t easily effect verifications of invariants before and after method calls, but we can easily arrange for them to be done at the beginning and end of method calls. Let�s look at a simple type, a 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 any Dollar instance d, either the expression d.getDollars() + d.getCents() == d.getAsCents() && g.getCents() < 100 holds true if d.getDollars() returns a non-negative value, otherwise the expression d.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.

<<  Page 2 of 4  >>


Sponsored Links



Google
  Web Artima.com   
Copyright © 1996-2014 Artima, Inc. All Rights Reserved. - Privacy Policy - Terms of Use - Advertise with Us