Title: Another Mediocre Assertion Mechanism for C
1Another Mediocre Assertion Mechanism for C
TOOLS Europe 2000 St. Malo, France, June 5-8,
2000
by Pedro Guerreiro pg_at_di.fct.unl.pt,
http//www-ctp.di.fct.unl.pt/pg Departamento de
Informática Faculdade de Ciências e
Tecnologia Universidade Nova de Lisboa 2528-114
Caparica, Portugal
2Why?
- If you believe in Design by Contract, you must
have a decent assertion mechanism in your
language. - C was designed primarily so that the author and
his friends would not have to program in
assembler, C, or various high-level programming
high-level languages, not for Design by
Contract. - Common assertion mechanisms for C use macros or
preprocessors. - You want your students to adopt Design by
Contract, not to frighten them away. - You really cannot live without preconditions to
remind you how you must validate your function
calls.
3Comments Are Not Enough
Yes, you can use comments in your C header
files
class StringBasic public Clonable public
StringBasic() StringBasic(const StringBasic
other) // ... StringBasic(const StringBasic
other, int startPos, int endPos)
// pre
other.ValidRange(startPos, endPos)
StringBasic(const StringBasic other, int
capacity)
// pre !other.Empty() capacity
gt 1 virtual void Select(int startPos, int
endPos) // pre ValidRange(startPos, endPos)
virtual bool ValidIndex(int x) const virtual
bool ValidRange(int x, int y) const virtual
int LastPosition(char c) const virtual int
LastPosition(char c, int start) const // pre
start lt Count() virtual char operator (int
x) // pre ValidIndex(x) virtual const
char First() const // pre !Empty() virtual
const char Last() const // pre !Empty()
virtual void Put(char c) // pre
!Full() //...
Comments are not enough, but do not underestimate
them.
4Make the Comments Executable
They are replaced by functions from class
Assertions
class Assertions public class Exception
public exception public Exception(const
stdstring label) exception (("Assertion
violation " label ".").c_str()) privat
e bool _enabled public // ...
virtual void require(bool b, const stdstring
label "") const if (_enabled !b)
throw AssertionsException("require "
label) //...
An exception from this class is thrown when an
assertion is violated.
This function represents preconditions. We also
have functions for postconditions, invariants,
etc.
5Class Assertions
Inherit from this class if you want to use
assertions.
class Assertions //... virtual bool
invariant() const return true
virtual void ensure(bool b, const stdstring
label "") const if (_enabled !b)
throw AssertionsException("ensure "
label) if (_enabled !invariant())
throw AssertionsException("invariant "
label) virtual void satisfy(bool b, const
stdstring label "") const if
(_enabled !b) throw AssertionsException
("satisfy " label) virtual void
check(bool b, const stdstring label "")
const if (_enabled !b) throw
AssertionsException("check " label)
This is the default invariant.
This function represents postconditions in
modifiers.
This function represents postconditions in
selectors. In selectors you do not check the
invariant.
This function represents arbitrary assertions.
6Inheriting From Class Assertions
class StringTools public Clonable, public
Assertions private int capacity char
p private virtual bool invariant() const
return Count() lt Capacity() public
// ... virtual bool Empty() const bool
result p 0 satisfy(result (Count()
0), "Empty") return result
virtual bool Full() const bool result
strlen(p) capacity - 1 satisfy(result
(Count() Capacity() - 1), "Full")
return result
This is an example with a class for strings, not
for stacks.
The ability to be cloned will be used later.
This redefines the inherited invariant.
We use local variable result to be able to refer
to the result of the function in the
postcondition.
These two functions illustrate the idiom for
postconditions in selectors.
7Preconditions, Postconditions
Some more functions
Remember precondition is require postcondition
for selector is satisfy postcondition for
modifier is ensure.
virtual void Select(int startPos, int endPos)
require(ValidRange(startPos, endPos))
Erase(endPos 1, Count() - 1) Erase(0,
startPos - 1) ensure(Count() endPos -
startPos 1, "Selected") virtual char
At(int x) const require(ValidIndex(x),
"Index is must be valid") char result
px satisfy(result px, "Char at given
position") return result virtual
void PutAt(char c, int x)
require(ValidIndex(x)) px c
ensure(At(x) c, "Put at right position")
This is a modifier.
This is a selector.
This is a modifier.
OK, but what about the old values?
8The old Notation
If we want to use the initial value of an
attribute, we must observe it. We recall the
observed value through its tag
virtual void Put(char c) require
(!Full(), "not Full at Put")
observe("count", Count()) int x
strlen(p) px c px1 0
ensure(!Empty(), "Not empty after Put")
ensure(Last() c, "Put at last")
ensure(Count() old("count") 1, "Count()
incremented") virtual void Erase(int
startPos, int endPos) require(ValidRange(s
tartPos, endPos)) observe("count",
Count()) memmove(pstartPos, pendPos1,
Count() - endPos) ensure(Count()
old("count") - (endPos - startPos 1),
"Erased")
Functions observe and old are inherited from
class Assertions.
Here we observe Count(). The tag is "count".
It is OK to share tags, provided you do not call
a function that uses the same tag.
Here we get the old value back, with function old.
OK, but where do the values go?
9old and observe
For storing the old values we use a class
generically derived from STL template class
stdmap. The key is the tag
class Assertions // ... private
stdmapltconst stdstring, intgt _old public
virtual void observe(const stdstring tag,
const int x) _oldtag x
virtual int old(const stdstring tag)
return _oldtag
This is simple, but we can only store integer
values.
10Storing the Object
class AssertionsMore public Assertions, public
Clonable private Clonable _old public
AssertionsMore() _old 0 virtual
AssertionsMore() delete _old
virtual void observe() delete _old
_old Clone() virtual const Clonable
old() const return _old
Most of the attributes we want to store for use
in postconditions are integers. Occasionally, in
modifiers, we may want to store the object. We
use class AssertionsMore for this
We can only store one object at a time!
The asserted class must provide function Clone.
The returned value is of type const Cloneble. It
must be dynamically cast before it can be used.
This part is new. It is not in the paper.
11Typecasting the Old Object
Typecasting the old object is safe, but clumsy
virtual void Put(char c) require
(!Full(), "not Full at Put") observe()
int x strlen(p) px c px1
0 ensure(!Empty(), "Not empty after Put")
ensure(Last() c, "Put at Last")
ensure(Count() (dynamic_castltconst
StringToolsgt(old())).Count() 1,
"Count() incremented")
Calling observe and old without an argument
refers to the functions inherited from class
AssertionsMore.
This is fine for programming, but it obfuscates
the specification.
This is not a good example. We need to store the
object initially only when the postcondition in a
modifier somehow compares the object with the old
object. This comparison should be provided by a
member function.
12Binary Selectors
A binary selector is one with one argument with
the same type as the object. For example,
StartsBy
virtual bool StartsBy(const StringTools other)
const return strlen(other.p) lt
strlen(p) strncmp(other.p, p,
strlen(other.p)) 0
Binary selectors often are overloaded, using as
argument a reference to the base type
The rule is that the actual argument must be an
object of the type of the target object, or the
dynamic cast will fail.
virtual bool StartsBy(const Clonable other)
const return StartsBy(dynamic_castltconst
StringToolsgt(other))
Comparisons between the object and the old object
are performed by binary selectors.
13No Typecasts in the Postconditions
When we add a character to a string, the existing
characters are unchanged
virtual void Put(char c) require
(!Full(), "not Full at Put") observe()
observe("count", Count()) int x
strlen(p) px c px1 0
ensure(!Empty(), "Not empty after Put")
ensure(Last() c, "Put at Last")
ensure(Count() old("count") 1, "Count()
incremented") ensure(StartsBy(old()),
"Existing chars untouched")
This is the second version of StartsBy, which
performs the typecast internally.
Exercise enrich the postconditions for function
Erase.
14Does It Work Under Inheritance?
Yes, most of the time, but there might be a
problem with redefined modifiers which call the
inherited version. In fact, if the postcondition
for the inherited version is evaluated before the
object is modified completely, some assertions
may fail. Consider function Put redefined in a
derived class that keeps the count in a member
variable
class StringToolsCounted public StringTools
private int count public // ...
virtual void Put(char c)
StringToolsPut(c) count
ensure(true, "Put in StringToolsCount")
Constructors, destructor, invariant, Copy,
assignment, Clone, go here.
The precondition is inherited.
This will check the invariant only.
15Recall
class StringTools public // ... virtual
bool Empty() const bool result p
0 satisfy(result (Count() 0),
"Empty") return result virtual void
Put(char c) // ... int x
strlen(p) px c px1 0
ensure(!Empty(), "Not empty after Put") //
...
class StringToolsCounted public StringTools
private int count public // ...
virtual int Count() const return count
virtual void Put(char c)
StringToolsPut(c) count
ensure(true, "Put in StringToolsCount")
This fails for an empty string when called from
there, because member count hasnt been
incremented yet.
16A Brute Force Fix
The postcondition in the base class must be
evaluated for a base class object (not for the
derived class object which may not be fully
modified yet). Lets make sure it is
class StringTools public virtual void
Put(char c) require (!Full(), "not Full
at Put") observe() observe("count",
Count()) int x strlen(p) px c
px1 0 StringTools current(this)
current.ensure(!current.Empty(), "Not empty
after Put") current.ensure(current.Last()
c, "Put at Last") current.ensure(current.Coun
t() old("count") 1, "Count() incremented")
current.ensure(current.StartsBy(old()),
"Existing chars untouched")
Local variable current is an object of class
StringTools, equal to the object of the
function, which can be of a derived class.
Using current as the object of ensure ensures
that the right invariant is considered.
17A Generic Cache (1)
Take a finite capacity cache, in which you store
unique objects by reverse order of arrival,
discarding older ones that overflow.
template ltclass Tgt class Cache public
AssertionsMore private stdlistltTgt items
int capacity bool inserted protected
virtual bool invariant() const return
Count() lt Capacity() public Cache(int
capacity) items(), capacity(capacity),
inserted(false)
It uses the full old mechanism.
virtual Cache() virtual Clonable
Clone() const return new Cache(this)
virtual int Count() const return
items.size() virtual int Capacity()
const return capacity
Lists from the STL.
This is a more substantial example. Will our
system hold?
18A Generic Cache (2)
virtual bool Full() const bool result
items.size() capacity satisfy(result
(Count() Capacity()), "Full") return
result virtual bool Empty() const
bool result items.size() 0
satisfy(result (Count() 0), "Empty")
return result virtual bool Has(const T
x) const return stdfind(items.begin(),
items.end(), x) ! items.end() virtual
bool Inserted() const return inserted
virtual const T Item() const return
items.begin()
So far, nothing spectacular.
19A Generic Cache (3)
virtual void Put(const T x)
observe() observe("had", Has(x))
observe("count", Count()) observe("full",
Full()) stdlistltTgtiterator where
stdfind(items.begin(), items.end(), x) if
(where ! items.end())
items.push_front(where)
items.erase(where) inserted false
check(Count() old("count")) else
if (Full()) items.pop_back()
items.push_front(x) inserted true
check(Count() old("count")
!old("full")) ensure(!Empty(), "not
empty") ensure(old("count") lt Count(),
"size must not decrease") ensure(Has(x),
"has element") ensure(Item() x, "element
at first position") ensure(Inserted()
!old("had"), "inserted") ensure(IsSuccessor(o
ld()), "is successor")
No preconditions.
Assertion check is used to check arbitrary
conditions.
Lots of postconditions.
Function IsSuccessor is a binary function. See
next page.
20A Generic Cache (4)
A cache value x is a successor of a cache value
y if x can be obtained by putting an element in y
virtual bool IsSuccessor(const CacheltTgt
other) require(!Empty(), "cache not
empty") stdlistltTgt temp(items)
stdlistltTgt previous(other.items)
stdlistltTgtiterator where stdfind(previous.
begin(), previous.end(), temp.front()) if
(where ! previous.end())
previous.erase(where) temp.pop_front()
if (temp.size() lt previous.size())
previous.pop_back() return temp
previous virtual bool IsSuccessor(const
Clonable other) require(!Empty(), "cache
not empty") return IsSuccessor(dynamic_castltc
onst CacheltTgtgt(other))
Here is the Clonable version.
21Other Issues
- Are the limitations very serious?
- What is the execution overhead?
- What if you want to split your classes in header
files and source files? - Can you apply these ideas to Java?
- What about loop invariants and loop variants?
- Do I use this on a daily basis?
22Conclusion