• No results found

An Example — List, Queue, and Stack

Let us look at a few new classes implemented from scratch using ooc so that we can appreciate the effort we are now spared. We start with a List implemented as a double-ended ring buffer that will dynamically expand as needed.

used

begin end

buf

count dim

begin and end limit the used part of the list, dim is the maximal buffer size, and count is the number of elements currently stored in the buffer. count makes it easy to distinguish between a full and an empty buffer. Here is the class descrip- tion List.d:

% ListClass: Class List: Object {

const void ** buf; // const void * buf [dim] unsigned dim; // current buffer dimension unsigned count; // # elements in buffer

unsigned begin; // index of takeFirst slot, 0..dim unsigned end; // index of addLast slot, 0..dim %

Object @ addFirst (_self, const Object @ element); Object @ addLast (_self, const Object @ element); unsigned count (const _self);

Object @ lookAt (const _self, unsigned n); Object @ takeFirst (_self);

Object @ takeLast (_self);

%— // abstract, for Queue/Stack Object @ add (_self, const Object @ element); Object @ take (_self);

86

___________________________________________________________________________7 The ‘‘ooc’’ Preprocessor — Enforcing a Coding Standard The implementation in List.dc is not very difficult. The constructor provides an ini- tial buffer:

% List ctor {

struct List * self = super_ctor(List, _self, app); if (! (self —> dim = va_arg(* app, unsigned)))

self —> dim = MIN;

self —> buf = malloc(self —> dim * sizeof * self —> buf); assert(self —> buf);

return self; }

Normally, the user will provide the minimal buffer size. As a default we defineMIN with a suitable value. The destructor eliminates the buffer but not the elements still in it:

% List dtor { %casts

free(self —> buf), self —> buf = 0; return super_dtor(List, self); }

addFirst() and addLast() add one element at begin or end:

% List addFirst { %casts

if (! self —> count)

return add1(self, element); extend(self);

if (self —> begin == 0)

self —> begin = self —> dim;

self —> buf[—— self —> begin] = element; return (void *) element;

}

% List addLast { %casts

if (! self —> count)

return add1(self, element); extend(self);

if (self —> end >= self —> dim) self —> end = 0;

self —> buf[self —> end ++] = element; return (void *) element;

}

Both functions share the code to add a single element:

static void * add1 (struct List * self, const void * element) {

self —> end = self —> count = 1;

return (void *) (self —> buf[self —> begin = 0] = element); }

The invariants are different, however: if count is not zero, i.e., if there are any ele- ments in the buffer, begin points to an element while end points to a free slot to

be filled. Either value may be just beyond the current range of the buffer. The buffer is used as a ring; therefore, we first map the variables around the ring before we access the buffer. extend() is the hard part: if there is no more room, we use realloc() to double the size of the buffer:

static void extend (struct List * self) // one more element {

if (self —> count >= self —> dim) { self —> buf =

realloc(self —> buf, 2 * self —> dim * sizeof * self —> buf); assert(self —> buf);

if (self —> begin && self —> begin != self —> dim) { memcpy(self —> buf + self —> dim + self —> begin,

self —> buf + self —> begin, (self —> dim — self —> begin)

* sizeof * self —> buf); self —> begin += self —> dim;

} else self —> begin = 0; } ++ self —> count; }

realloc() copies the pointers stored in buf[], but if our ring does not start at the beginning of the buffer, we have to use memcpy() to shift the beginning of the ring to the end of the new buffer.

The remaining functions are much simpler. count() is simply an access func- tion for the count component. lookAt() uses an arithmetic trick to return the proper element from the ring:

% List lookAt { %casts

return (void *) (n >= self —> count ? 0 :

self —> buf[(self —> begin + n) % self —> dim]); }

takeFirst() and takeLast() simply reverse the invariants of the corresponding add functions. Here is one example:

% List takeFirst { %casts

if (! self —> count) return 0;

—— self —> count;

if (self —> begin >= self —> dim) self —> begin = 0;

return (void *) self —> buf[self —> begin ++]; }

88

___________________________________________________________________________7 The ‘‘ooc’’ Preprocessor — Enforcing a Coding Standard List demonstrates that ooc gets us back to dealing with the implementation issues of a class as a data type rather than with the idiosyncrasies of an object- oriented coding style. Given a reasonable base class, we can easily derive more problem-specific classes. List introduced dynamically linked methods add() and take() so that a subclass can impose an access discipline. Stack operates on one end:

Stack.d

% ListClass Stack: List { %}

Stack.dc

% Stack add {

return addLast(_self, element); }

% Stack take {

return takeLast(_self); }

%init

Queue can be derived from Stack and overwrite take() or it can be a subclass of List and define both methods. List itself does not define the dynamically linked methods and would; therefore, be called an abstract base class. Our selectors are robust enough so that we will certainly find out if somebody tries to use add() or take() for a List rather than a subclass. Here is a test program demonstrating that we can add plain C strings rather than objects to a Stack or a Queue:

#include "Queue.h"

int main (int argc, char ** argv) { void * q; unsigned n; initQueue(); q = new(Queue, 1); while (* ++ argv) switch (** argv) { case ’+’: add(q, *argv + 1); break; case ’—’: puts((char *) take(q)); break; default: n = count(q); while (n —— > 0)

{ const void * p = take(q); puts(p), add(q, p); }

} return 0; }

If a command line argument starts with + it is added to the queue; for a− one ele- ment is removed. Any other argument displays the contents of the queue:

$ queue +axel — +is +here . — . — . axel is here is here here

Replacing the Queue by a Stack we can see the difference in the order of the removals:

$ stack +axel — +is +here . — . — . axel is here here is is

Because a Stack is a subclass of List there are various ways to nondestructively display the contents of the stack, for example:

n = count(q); while (n —— > 0)

{ const void * p = takeFirst(q); puts(p), addLast(q, p);

}

7.8 Exercises

It is an interesting exercise to combine Queue with Point and Circle for a skeleton graphics program with redrawing capabilities.

The reports−r and include can be modified to implement the opaque structure definitions suggested in section 4.6.

The init reports can be modified to generate a method to display a Class struc- ture.

Selectors and superclass selectors are generated by reports in etc.rep. They can be modified to provide an execution trace or to experiment with various levels of parameter checking.

The ooc command script and the modules main.awk and report.awk can be changed so that an argument−x results in a report x.rep to be loaded, interpreted, and removed. Given that change, a new report flatten.rep can show all methods available to a class.

91 ___________________________________________________________________________

8

Dynamic Type Checking

Defensive Programming