Re: polymorphic structs with "methods"



"Peter Michaux" <petermichaux@xxxxxxxxx> wrote in message news:78aa7392-c661-4558-a428-36e01d86b33d@xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
/*
I want to have heterogeneous lists but treat all nodes the same
without checking some sort of struct "type" member and then using a
switch statement to call the appropriate function for that type. This
is in an effort to make my code more modular. Updating the switch
statement when a new "type" is added is not appealing. I've been
working on an idea (surely not original) about having polymorphic
struct "objects" that have function pointer members for "methods". My
code example is below and I would like to know where it sits in the
range from "hideous worst practice ever" to "yep people do that sort
of thing". If there are known improvements I could make I would
appreciate any comments, a link to a web page or book title.
*/

This is sometimes done, but if you need to do this extensively, think twice about whether you really, really need to use C rather than some language (like C++) that has all of these things built-in.

For an example of how ugly it can get when you try full-on OOP with C, look at the source for a GTK+ program -- or at GTK+'s source itself.

Part of programming is learning to use the right tool for the job. C makes a great hammer, but sometimes what you really need is a screwdriver.

#include <stdlib.h>
#include <stdio.h>
#include <string.h>

typedef struct _Object {
void (*prettyPrint)(struct _Object *);
void (*destroy)(struct _Object *);
} Object;

/***********************************/

typedef struct _Number {
/* obj member must be first in this struct
for down casts of Number to Object */
Object obj;
double val;
} Number;

void prettyPrintNumber(Object *obj) {
Number *num = (Number *)obj;
printf("the number is: %g\n", num->val);
}


You shouldn't allow any Object to be printed as if it were a Number, only Numbers.

void prettyPrintNumber(Number *num) {
printf("the number is: %g\n", num->val);
}

This way, absent a bug-hiding cast, you'll get a compile-time error when you call this the wrong way. Technically it's broken, since you'll end up assigning it to a "method" pointer that wants an Object*, but that just means you need a cast there; all pointers-to-struct have the same representation, so it's safe in practice.

void destroyNumber(Object *obj) {
Number *num = (Number *)obj;
free(num);
}
Number *createNumber(double val) {
Number *num = (Number *)malloc(sizeof(Number));
num->obj.prettyPrint = prettyPrintNumber;
num->obj.destroy = destroyNumber;
num->val = val;
return num;
}

You've already messed up here, according to the general OO model... There is no constructor or destructor for Object, which you should be chaining to in the constructor and destructor for Number. (This gets important once you have multiple layers of inheritance. If you want multiple inheritance too, it gets even uglier.)

Object *constructObject(Object *obj) {
obj->prettyPrint = prettyPrintObject;
obj->destroy = destroyObject;
}

Object *newObject(void) {
Object *obj = malloc(sizeof (Object));
if (!obj) do_something();
return constructObject(obj);
}

void destroyObject(Object *obj) { }
void prettyPrintObject(Object *obj) { }

Now that you have those, the relevant items for Number:

Number *constructNumber(Number *num, double val) {
constructObject(&num.obj);
num->obj.prettyPrint = prettyPrintNumber; /* cast left out for clarity */
num->obj.destroy = destroyNumber; /* cast left out for clarity */
num->val = val;
return num;
}


Number *newNumber(double val) {
Number *num = malloc(sizeof *num);
if (!num) do_something();
return constructNumber(num);
}

void destroyNumber(number *num) {
/* val will destroy itself */
destroyObject(num->obj);
}

The reason I have broken out "new" and "construct" is that you can't chain to a parent constructor _that also allocates_ using the above model. I prefer to have the caller do (de)allocation, but I've kept a "new" form to make adapting your code easier. There's a different form that makes the obj member a pointer which solves that (and also solves multiple inheritance, but introduces other problems with polymorphism...).

All of the above comments apply to your String objects, so I've removed that code.

/***********************************/

int main(void)
{
printf("hello, world\n");

Object *n = (Object *)createNumber(21);

With my model:

Object *n = (Object *)newNumber(21);

You could also do this:

Number foo; constructNumber(&foo, 21);
Object *n = (Object *) foo;

/* don't need to know n is a Number to print it */
n->prettyPrint(n);

Object *s = (Object *)createString("test");

See above.

s->prettyPrint(s);

n->destroy(n);
s->destroy(s);

Since my destructors above don't actually deallocate:

n->destroy(n); free(n);
s->destroy(s); free(s);

If your Number and String are automatics, you can leave out the call to free(). Or you might separate destroy() and destruct(), with the former doing a free() after calling the latter, and the latter chaining up to its parents' destruct()s.

return 0;
}

/*
Thanks,
Peter
*/


--
Stephen Sprunk "God does not play dice." --Albert Einstein
CCIE #3723 "God is an inveterate gambler, and He throws the
K5SSS dice at every possible opportunity." --Stephen Hawking

.



Relevant Pages

  • polymorphic structs with "methods"
    ... switch statement to call the appropriate function for that type. ... typedef struct _Object { ... Object obj; ... void prettyPrintNumber{ ...
    (comp.lang.c)
  • Re: Efficency and the standard library
    ... "Richard Heathfield" wrote in message ... have been to use pointers to void in each node to point to the ... struct node* next; ... struct object* obj) ...
    (comp.lang.c)
  • Re: [RFC] timers, pointers to functions and type safety
    ... void * and update SETUP_TIMER accordingly; ... struct list_head entry; ... this relies more on gcc specific behavior than we ... as void *obj. ...
    (Linux-Kernel)
  • Re: trying to construct an array.....corrected
    ... > still can't figgure out if struct is really what I need. ... > void Detect_type ... //return type should be char * since you are returning value ... //of which num is num and string are members. ...
    (comp.lang.c)
  • Re: back once again...
    ... reference to type ... a, signed char ... int dycObjectP(dyt obj); ... void dycBeginClass; ...
    (comp.lang.misc)