|
|
|
Sponsored Link •
|
|
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.
#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 anyDollarinstance d, either the expressiond.getDollars() + d.getCents() == d.getAsCents() && g.getCents() < 100holds true ifd.getDollars()returns a non-negative value, otherwise the expressiond.getDollars() - d.getCents() == d.getAsCents() && g.getCents() < 100holds 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.
|
Sponsored Links
|