|
|
|
|
|
Project 0: Part 3 Implementation Details
← Back to main assignment
To help make things clear, the main writeup describes the goals of
Part 3 using what
should be reasonably familiar C constructs.
This addendum explains some unfamiliar C constructs
that make implementation cleaner and easier.
That is, you should implement Part 3 in the style explained
here, not the one used in the main writeup.
The primary implementation problem has to do with type checking,
and specifically polymorphism (or lack thereof) in C. As an example, the second
argument to math_call is either of two
kinds of structs: one encoding two integer
arguments, and one encoding four integer arguments.
The issue is what the declared type of the second
argument should be:
int math_call(int function_name, ??? args, int* result);
The main assignment writeup addresses this by defeating
type checking entirely, using void*:
int math_call(int function_name, void* args, int* result);
Now the second argument can be a pointer to anything. The downside
of this that the user of math_call doesn't get the benefits
of type checking, and so subtle errors can easily arise.
A better solution than void* is to use the primitive form of
variable polymorphism that C supports. In particular, we can use
the union type. C's union is related to struct,
in the sense that you declare a set of fields that comprise the union.
For structs, a variable of that struct type contains all of the fields.
For unions, a variable of that union type contains just one of the fields -
any one. (Technically, C allocates just enough memory to hold the single, largest field,
then uses that memory as though it held whichever field the program references.)
Here's mathtable.h, from the skeleton code distribution:
#ifndef MATHTABLE_H
#define MATHTABLE_H
/* function type */
typedef enum {
ADD,
SUB,
MULT,
SLOPE,
MAX_FUNC_ID /* to ease boundary checking */
} mathfunctype_t;
/* generic argument type */
typedef struct {
int argc; /* we will choose arguments according to this value */
union {
int arg2[2];
int arg4[4];
} arg;
} matharg_t;
extern int math_call(mathfunctype_t,const matharg_t *arg,int *res);
#endif /* MATHTABLE_H */
This declares matharg_t as a struct that has an int field named
argc and either a length two or a length four int array.
C does not know which it holds; its up to the programmer to keep usage consistent.
Basically, whatever code initializes the matharg_t sets argc to either
2 or 4, depending on how many arguments it wants to encode. The code receiving the
matharg_t looks at the value of argc to determine how to correctly
use the union. C doesn't understand that this is how the struct is being used,
so will not detect any misuses, but if the code is correct everything works fine.
You reference the fields of the union just like you do the fields of a struct,
for example:
matharg_t myArgs;
myArgs.arg.arg2[0] = 10;
myArgs.arg.arg4[3] = 100;
(Note that those lines are legal C, but don't make much sense. myArgs should
be considered to have
values in either arg2 or arg4, but not both (at the same time).)
Note that this has solved the type definition issue for math_call: the second
argument is now always a pointer to a matharg_t, which can encode either
two or four arguments, depending on how it's used.
mathtable.h includes a second change from what is suggested in the main
assignment writeup: it uses an enum to define symbolic names for function
indices, rather than a series of #defines. The enum is convenient
because it automatically makes each successive value increment by one, relieving you,
the programmer, of the burden of making sure the list is consistent (e.g., doesn't contain duplicates).
|