Previous section   Next section

Imperfect C++ Practical Solutions for Real-Life Programming
By Matthew Wilson
Table of Contents
Chapter 1.  Enforcing Design: Constraints, Contracts, and Assertions


1.3. Runtime Contracts: Preconditions, Postconditions, and Invariants

"If all the routine's preconditions are met by the caller, the routine shall guarantee that all postconditions (and [any] invariants) will be true when it completes." —Hunt and Thomas, The Pragmatic Programmers [Hunt2000].

If we can't get compile-time enforcement, then the alternative is runtime enforcement. A systematic approach to runtime enforcement is to specify function contracts. A function contract defines exactly what conditions the caller must fulfill before calling the function (the preconditions of the function) and exactly what conditions the caller can expect upon return from the function (the postconditions of the function). The definition of contracts, and their enforcement, is the basis of DbC [Meye1997].

A precondition is what must be true in order for a function to fulfill its contract. It is the caller's responsibility to ensure that the precondition is met. The callee may assume that its precondition has been met, and has responsibility for correct behavior only in that case. This is a very important point, which is stressed in [Meye1997]. If the caller does not fulfill the terms of the precondition, the callee can legitimately do anything at all. Usually this involves the firing of an assertion (see section 1.4), which may terminate the program. This seems like a scary prospect, and programmers new to DbC often feel more than a little uneasy until you ask them what they would have a function do with conditions that violate its contract. The fact of the matter is that the more narrow the contract, and the more severe the response to a violation, the better quality the software. Making this leap is the most, and probably the only, difficult step when converting to DbC.

A postcondition is what will be true after the function has been called. It is the callee's responsibility to ensure the postcondition is met. The caller may assume that the postcondition has been met when the function returns control to it. The caller is not responsible for ensuring that the callee has fulfilled its contract. In the real world, it is sometimes necessary to hedge one's bets, for example, when calling into third-party plug-ins in live application servers. However, I think the principle still holds true. In fact, one can argue that a valid response to a contract-violating plug-in is to unload it, and send an e-mail to the managers of the hosting company and the third-party plug-in vendor. Since we can do anything in response to a violation, why not?

Preconditions and postconditions can apply to class member functions as well as free functions, which is a good thing for C++ (and object-oriented programming in general). In fact, there is a third component to DbC, which pertains only to classes: the class invariant. A class invariant is a condition, or set of conditions, which together always hold true for an object in a well-defined state. By definition, it is the role of the constructor to ensure that the instance is put into a state that conforms to the invariant, and the responsibility of the (public) member functions to ensure that the instance continues to be in such a state upon their completion. Only during construction, destruction, or the execution of one of its member functions may the invariant not hold true.

In some circumstances it is appropriate to define invariants that have wider scope than that of an individual object's state. In principle, an invariant can apply to the entire state of the operating environment. In practice, however, such things are very rare, and it is usual to simply deal with class invariants. Throughout the remainder of this chapter, and the rest of the book, when I speak of invariants, I am referring to class invariants.

It is possible to provide invariants for types that are partially, or not at all, encapsulated (see sections 3.2 and 4.4.1), which are enforced in the associated API functions (along with the function preconditions). Indeed, when using such types, invariants are a very good idea, since their lack of encapsulation increases the likelihood of their being abused. But the ease with which such invariants can be circumvented ably illustrates why such types should generally be avoided. Indeed, some [Stro2003] would say that if there is an invariant, public data make little sense in object-oriented programming; encapsulation is both about hiding implementation and protecting invariants. The use of properties (see Chapter 35) is one way in which we can have the semblance of public member variables, perhaps for reasons of structural conformance (see section 20.9), but still have the enforcement of invariants.

The action you choose to take upon the violation of preconditions, postconditions, or invariants is up to you. It may be logging the information to a file, throwing an exception, or sending the missus an SMS to let her know you'll be debugging late tonight. Commonly it takes the form of an assertion.

1.3.1 Preconditions

Precondition testing is easily done in C++. We've seen several examples of this already in the book. It's as simple as using an assertion:



template< . . . >


typename pod_vector<. . .>::reference pod_vector<. . .>::front()


{


  MESSAGE_ASSERT("Vector is empty!", 0 != size());


  assert(is_valid());


  return m_buffer.data()[0];


}



1.3.2 Post-conditions

This is where C++ stumbles badly. The challenge here is to capture the values of the out parameters and the return value at the epoch of function exit. Of course, C++ provides the singularly useful Resource Acquisition Is Initialization (RAII) mechanism (see section 3.5), which guarantees that a destructor for an object on the stack will be called when execution exits its defining scope. This means we can get some of the way toward a workable solution, in the mechanics at least.

One option would be to declare monitor objects, which take references to the out parameters and return value.



int f(char const *name, Value **ppVal, size_t *pLen)


{


  int               retVal;


  retval_monitor    rvm(retVal, . . . policy . . . );


  outparam_monitor  opm1(ppVal, . . . policy . . . );


  outparam_monitor  opm2(pLen, . . . policy . . . );


  . . . // The body of the function


  return retVal;


}



The policies would check whether the variable is NULL, or non-NULL, or is within a certain range, or one of a set of values, and so on. Notwithstanding the difficulties of getting just this far, there are two problems. The first one is that the destructor of rvm will enforce its constraint via the reference it holds to the return value variable retVal. If any part of the function returns a different variable (or a constant), rvm will inevitable report a failure. To work correctly we would be forced to write all functions to return via a single variable, which is not to everyone's taste and is not possible in all circumstances.

The main problem, however, is that the various postcondition monitors are unconnected. Most function postconditions are composite: the values of individual out parameters and the return value are only meaningful in concert, for example,



assert(retVal > 0 || (NULL == *ppVal && 0 == *pLen));



I'm not even going to start suggesting how the three individual monitor objects could be combined in such a way as to enforce a wide variety of postcondition states; such things might be an exciting challenge for a template meta-programmer doing postgraduate research, but for the rest of us we're way past the level at which the complexity is worth the payoff.

Imperfection: C++ does not provide suitable support for postconditions


In my opinion, the only reasonable, albeit prosaic, solution is to separate the function and the checks by using a forwarding function, as shown in Listing 1.5.

Listing 1.5.


int f(char const *name, Value **ppVal, size_t *pLen)


{


  . . . // Do f() pre-conditions


  int retVal = f_unchecked(name, ppVal, pLen);


  . . . // Do f() post-conditions


  return retVal;


}


int f_unchecked(char const *name, Value **ppVal, size_t *pLen)


{


  . . . // The semantics of f


}



In real code, you may want to elide all the checks in builds where you're not enforcing DbC, and for this we'd need the preprocessor:

Listing 1.6.


int f(char const *name, Value **ppVal, size_t *pLen)


#ifdef ACMELIB_DBC


{


  . . . // Do f() pre-conditions


  int retVal = f_unchecked(name, ppVal, pLen);


  . . . // Do f() post-conditions


  return retVal;


}


int f_unchecked(char const *name, Value **ppVal, size_t *pLen)


#endif /* ACMELIB_DBC */


{


  . . . // The semantics of f


}



It's anything but pretty, but it works, and is easy to incorporate into code generators. The issue gets more complex when working with overridden class methods, since you then have to face the question as to whether you enforce the pre- and postconditions of parent classes. That's something that must be determined on a case-by-case basis, and is outside the scope of this discussion.[6]

[6] I confess to some self-serving cowardice on this point, but with good reason. Even in languages where the use of DbC is mature, there is equivocation on the usefulness of, and indeed the mechanisms for, relating contracts between levels of an inheritance hierarchy. Furthermore, the proposal to add DbC to C++ is, at the time of writing, only just about to be entered for consideration [Otto2004], so I believe it's inappropriate to discuss these details further here.

1.3.3 Class Invariants

Class invariants are almost as easy to do in C++ as preconditions. It's my practice to define a method is_valid() for classes, as in:



template<. . . >


inline bool pod_vector<. . .>::is_valid() const


{


  if(m_buffer.size() < m_cItems)


  {


    return false;


  }


  . . . // Further checks here


  return true;


}



This is then invoked in an assertion within all the public methods of the class, upon method entry, and prior to method exit. I tend to do the class invariant check just after the precondition check (as shown in section 1.3.1):



template< . . . >


inline void pod_vector<. . .>::clear()


{


  assert(is_valid());


  m_buffer.resize(0);


  m_cItems = 0;


  assert(is_valid());


}



An alternative strategy is to place the assertions within the invariant function itself. Unless you have a sophisticated assertion (see section 1.4), however, this leaves you with the choice of providing assertion information (file + line + message) on the offending condition or in the offending method. I prefer the latter, because invariant violations are rare. However, you may choose the former, in which case you'd wish to place the assertions inside the is_valid() member.

In fact, there's a reasonable half-way house, which I often use in environments where there's a well-known logging/tracing interface available (see section 21.2), which is to log the details of the invariant violation in the is_valid() member, and fire the assertion in the offending method.

Unlike out-parameter and return value checking, it's easy to use RAII (see section 3.5) to automate the class invariant check as part of the postcondition validation upon method exit, as in:



template< . . . >


inline void pod_vector<. . .>::clear()


{


  check_invariant<class_type> check(this);


  m_buffer.resize(0);


  m_cItems = 0;


}



The downside is that the enforcement is carried out within the constructor(s) and destructor of the check_invariant template instantiation, which means that simple assertion mechanisms that use the preprocessor to derive the _ _FILE_ _ and _ _LINE_ _ information (see section 1.4) may give misleading messages. However, it is not too challenging to implement macro + template forms that are able to display the appropriate location for the assertion failure, even incorporating the nonstandard _ _FUNCTION_ _ preprocessor symbol with those compilers that provide it.

1.3.4 Checking? Invariably!

In [Stro2003] Bjarne Stroustrup made the important observation that invariants are only necessary for classes that have methods, and not for simple structures that merely act as aggregates of variables. (The Patron type we look at in section 4.4.2 does not need an invariant, for example.) In my opinion, the converse is also true: I would say that any class that has methods should have a class invariant. In practice, though, there's a base limit to this. If your class holds a single pointer to some resource, then it is either going to have a NULL pointer, or a non-NULL pointer. Unless your class invariant method can use an external source of validity on the non-NULL pointer, there's not much for it to do. In that case, it's up to you whether you have a stub class invariant method, or don't bother. If your class is evolving, you're probably easing future refinement efforts by putting in a stub that'll be expanded later. If you use a code generator, then I would suggest that you simply generate a class invariant method, and calls to it, in all cases.

The benefits of using a class invariant method rather than individual assertions dotted around the class implementation are pretty obvious. Your code is easier to read, has a consistent look between the implementation of different classes, and is easy to maintain because you define the class invariant condition(s) in a single place for each class.

1.3.5 To DbC, or Not to DbC?

The picture of runtime contracts that I've painted thus far has contained the implicit assumption that one will create, after appropriate testing, a build of one's system that will have the DbC elements elided out by the preprocessor.

In fact, there is much equivocation as to whether any build ever should be made that does not enforce DbC [Same2003]. The argument is, to borrow a borrowed analogy [Same2003], that the contractual enforcements in DbC are equivalent to fuses in an electrical system, and one would not remove all the fuses from a sophisticated piece of electrical equipment just prior to deploying it.

The difference between assertions and fuses is that the assertions involve runtime tests, which have specific non-zero cost. Although the alloy in a fuse may have slightly different resistivity than the ambient characteristics of the system in which it serves, it is not reasonable to say that the costs involved are analogous. Striking the right balance is, in my opinion, something to be determined on a case-by-case basis. That's why the code examples shown in this section contain the ACMELIB_DBC symbol, rather than using NDEBUG (or _DEBUG), because the use of DbC should not be directly coupled to the binary notion of debug and release in your software. When you use it, and when you elide it, is an issue for you to determine for yourself.[7]

[7] In ISE Eiffel 4.5 you cannot remove preconditions; presumably the rationale is that preconditions can react before program state becomes undefined and, therefore, it makes sense to continue the program by catching the violation exception.

1.3.6 Runtime Contracts: Coda

Although we've seen that C++ is pretty broken for postconditions, it is reasonably well suited for precondition and class invariant testing. In practice, using these two together yields a great deal of the power of DbC. The absence of postcondition testing of return values and out-parameters, while disappointing, is not terribly inhibitive. If you must have it, you can resort to the preprocessor, as we saw in section 1.3.3.

For invariants, as with constraints, we make our lives easier by using a layer of indirection: a macro for constraints, a member function for invariants. It's thus simple to add support for new compilers, or to modify the internals of one's class, and we hide all the gunk of the mechanisms inside the class invariant method.


      Previous section   Next section