We now clean up the void * graph class implementation by introducing a new ACCESS class. It is clear that the method introduced above is quite cumbersome as the number of access methods grows. We will now develop a pure virtual parent class called ACCESS_VOID from which we can develop as many children as we need to address specific access needs.
Consider the following class defintions: we begin by defining appropriate typedefs.
#define INCLUDE_ACCESS_VOID class ostream; class istream; typedef void *Arbent; typedef int(* EQ_FN)(Arbent,Arbent); //Are object types the same? typedef ostream&(* DISP_FN)(ostream&, Arbent); //How do we display this type? typedef void(* ENTRY_FN)(Arbent *,Arbent); //How do we type the data?
We do not really use the typdefed methods listed above, but it is very convenient to list them as a reminder of the standrad prototypes we are using for these access methods. The parent class, ACCESS_VOID consists of an empty constructor and destructor followed by the three pure virtual methods, equal, show and entry.
class ACCESS_VOID
{
public:
ACCESS_VOID(){;};
~ACCESS_VOID(){;};
virtual int equal(Arbent,Arbent) = 0;
virtual ostream& show(ostream&,Arbent) = 0;
virtual void entry(Arbent *,Arbent) = 0;
};
The first child class is ACCESS_FLOAT. Note that its methods MUST be prototyped exactly the same way as the ones in the parent. We are allowed to change the return type, but not the argument types. Now this is an important point too for the philosophy of this typeless environment. We will be using this in the typeless graph class. Hence, all information about the specific type of data MUST be encapsulated away from the graph data type itself and the only place type information can be used is in a specific access method used to deal with that type. So here, the methods in this class present a typeless interface to the world and all the hard work of understanding how to work with float * data is buried in the implementation of the class agents (which will be in the file graphaccess.c since this header file is named graphaccess.h).
class ACCESS_FLOAT : public ACCESS_VOID
{
public:
ACCESS_FLOAT(){;};
~ACCESS_FLOAT(){;};
int equal(Arbent,Arbent);
ostream& show(ostream&,Arbent);
void entry(Arbent *,Arbent);
};
Let's look at the implementation of these methods:
//ACCESS_FLOAT
int ACCESS_FLOAT::equal(Arbent a,Arbent b)
{ return( *((float *)a) == *((float *)b) );}
void ACCESS_FLOAT::entry(Arbent *a,Arbent b)
{ (float *)(*a) = new float; *a = b;}
ostream& ACCESS_FLOAT::show(ostream& out, Arbent a)
{ out << *((float *)a) << endl; return out; }
Note that use of the type float * only occurs inside the methods themselves. The most interesting one is the entry method. Note the void * node data is called a and we are sending its address &a into this function. Now we want to allocate a chunk of memory large enough to store the incoming float * data b and we want to be able to interpret what is stored in the starting address we place in *a as float * data. So we cast *a to be of type float * and then store in *a the starting address of the chunk of memory retruned by the new operator for data of type float. Then now that the block of memory assigned to *a is the right size, we reset the starting address stored in *a to be the incoming address for b. Similar arguments explain the other methods. The explanations for more complex data will be more itneresting and we will go through an example of that momentarily.
Next, we can develop access methods for type double *. We will simply list these as the class and its implementation are essentially copies of the float * case with obvious changes.
class ACCESS_DOUBLE : public ACCESS_VOID
{
public:
ACCESS_DOUBLE(){;};
~ACCESS_DOUBLE(){;};
int equal(Arbent,Arbent);
ostream& show(ostream&,Arbent);
void entry(Arbent *,Arbent);
};
//ACCESS_DOUBLE
int ACCESS_DOUBLE::equal(Arbent a,Arbent b)
{ return( *((double *)a) == *((double *)b) ); }
void ACCESS_DOUBLE::entry(Arbent *a,Arbent b)
{ (double *)(*a) = new double; *a = b;}
ostream& ACCESS_DOUBLE::show(ostream& out, Arbent a)
{ out << *((double *)a) << endl; return out; }
The next example uses more complex data. Now this data is not very realistic and is just used to check that this mechanism for dealing with arbitrary node and edge data is working. For purposes of illustration then we define some structures
struct MyVertex{
float x;
float y;
};
struct NodeStruct{
int load;
float time;
MyVertex V;
};
struct EdgeStruct{
float capacity;
float loss;
float time;
MyVertex V;
};
We can now develop access methods for the structures NodeStruct and EdgeStruct. These have more interesting implementations.
The class defintions are very similar to what we have already done.
class ACCESS_NODESTRUCT : public ACCESS_VOID
{
public:
ACCESS_NODESTRUCT(){;};
~ACCESS_NODESTRUCT(){;};
int equal(Arbent,Arbent);
ostream& show(ostream&,Arbent);
void entry(Arbent *,Arbent);
};
class ACCESS_EDGESTRUCT : public ACCESS_VOID
{
public:
ACCESS_EDGESTRUCT(){;};
~ACCESS_EDGESTRUCT(){;};
int equal(Arbent,Arbent);
ostream& show(ostream&,Arbent);
void entry(Arbent *,Arbent);
};
#endif
Let's examine the implementations of these methods.
//ACCESS_NODESTRUCT
int ACCESS_NODESTRUCT::equal(Arbent a,Arbent b)
{
NodeStruct *t = (NodeStruct *)a;
NodeStruct *s = (NodeStruct *)b;
return( (t->load==s->load)
&&(t->time==s->time)
&&(t->V.x==s->V.x)
&&(t->V.y==s->V.y) );
}
void ACCESS_NODESTRUCT::entry(Arbent *a,Arbent b)
{
(NodeStruct *)(*a) = new NodeStruct;
NodeStruct *t = (NodeStruct *)b;
((NodeStruct *)(*a))->load = t->load;
((NodeStruct *)(*a))->time = t->time;
((NodeStruct *)(*a))->V.x = t->V.x;
((NodeStruct *)(*a))->V.y = t->V.y;
}
ostream& ACCESS_NODESTRUCT::show(ostream& out, Arbent a)
{
NodeStruct *t = (NodeStruct *)a;
out << "Load = " << t->load << endl;
out << "Time = " << t->time << endl;
out << "Vertex = (" << t->V.x << "," << t->V.y << ")" << endl;
return out;
}
The most interesting method is again the entry method. Let's look at it line by line. The line below takes the address of the void * data a in the node, casts it to the type NodeStruct * and then loads that address with the starting address of a block of memory sufficient to hold an item of NodeStruct data. This starting address is returned by the new operator.
(NodeStruct *)(*a) = new NodeStruct;
Now remember that the data b comes in as void *, so we must cast it to its proper type before we use it. We then reset the address stored in *a to be the address of the b data.
NodeStruct *t = (NodeStruct *)b;
How do we use the *a data? Well, remember that a all by itself is always vodi *. So a line like (*a)->load is nonsensical as the compiler will righfully complain that *a is not aggregate data and in fact is void *! So we must provide the extra information that enables us to interpret the information stored in the void * data a. To do this, first we cast *a to be type NodeStruct * with the command:
(NodeStruct *)(*a)
and then we surround that with parenthesis so that when we can compute correct offsets into the NodeStruct data structure via the -> operator.
( (NodeStruct *)(*a) )
Only at this point can we correctly handle -> requests.such as
((NodeStruct *)(*a))->load
We use this technique to correctly fill the block of memory whose starting address is *a in the following lines:
((NodeStruct *)(*a))->load = t->load; ((NodeStruct *)(*a))->time = t->time; ((NodeStruct *)(*a))->V.x = t->V.x; ((NodeStruct *)(*a))->V.y = t->V.y;
Finally, note the show method uses a similar trick: The line
NodeStruct *t = (NodeStruct *)a;
allows us to interpret the block of memory whose starting address is *a as type NodeStruct and to use it we simply set a temporary pointer t equal to this address and then use t in the output commands.
out << "Load = " << t->load << endl;
out << "Time = " << t->time << endl;
out << "Vertex = (" << t->V.x << "," << t->V.y << ")" << endl;
return out;
For completeness, we list the ACCESS_EDGESTRUCT methods also.
//ACCESS_EDGESTRUCT
int ACCESS_EDGESTRUCT::equal(Arbent a,Arbent b)
{
EdgeStruct *t = (EdgeStruct *)a;
EdgeStruct *s = (EdgeStruct *)b;
return( (t->capacity==s->capacity)
&&(t->loss==s->loss)
&&(t->time==s->time)
&&(t->V.x==s->V.x)
&&(t->V.y==s->V.y) );
}
void ACCESS_EDGESTRUCT::entry(Arbent *a,Arbent b)
{
(EdgeStruct *)(*a) = new EdgeStruct;
EdgeStruct *t = (EdgeStruct *)b;
((EdgeStruct *)(*a))->capacity = t->capacity;
((EdgeStruct *)(*a))->loss = t->loss;
((EdgeStruct *)(*a))->time = t->time;
((EdgeStruct *)(*a))->V.x = t->V.x;
((EdgeStruct *)(*a))->V.y = t->V.y;
}
ostream& ACCESS_EDGESTRUCT::show(ostream& out, Arbent a)
{
EdgeStruct *t = (EdgeStruct *)a;
out << "Capacity = " << t->capacity << endl;
out << "Loss = " << t->loss << endl;
out << "time = " << t->time << endl;
out << "Vertex = (" << t->V.x << "," << t->V.y << ")" << endl;
return out;
}