Object-oriented design patterns
16 points by kaycebasques
16 points by kaycebasques
One thing I like doing whenever I build vtables by hand is to always have the vtable struct as the first member of the object.
struct vtable {...};
struct obj_a {
struct vtable vtable;
char *name;
};
struct obj_b {
struct vtable vtable;
int count;
};
This way I can easily build generic functions which only require a pointer to the vtable since &obj_a == &obj_a.vtable
void do_generic(struct vtable *obj) {
obj->do(obj);
};
...
do_generic(&obj_a);
It’s not super type safe and you might have to define your member functions as always taking void*
as the first parameter but it’s pretty ergonomic.
The alternative looks like:
void do_generic2(void *obj, struct vtable *vtable) {
vtable->do(obj);
}
...
do_generic2(&obj_a, &obj_a.vtable);
Stuffing a copy of the vtbl inside every object is going to waste a huge amount of space if you have lots of objects. (It’s also unkind to the CPU’s indirect call predictor.)
The article is a bit weird in that it puts the vtbl pointer in inconsistent positions inside each kind of object. In my experience it’s more common to have a base type like
struct object {
struct vtbl *vtbl;
};
then all types either have the bare vtbl as the first member, or embed the base type as their first member to avoid some casts like
thing->obj.vtbl->method(&thing->obj, args);
The call syntax is usually too painful, so the BSDs (like CLOS or Julia) typically define a generic function to do the dynamic dispatch like
ret method(struct obj *obj, args) {
return obj->vtbl->method(obj, args);
}
Another way to avoid casts is to pass the address of the vtbl pointer, which is the same as the address of the object, tho the extra indirection can be rather ugly
ret method(struct vtbl **vvtbl, args) {
return (**vvtbl).method(vvtbl, args);
}
// …
method(&obj.vtbl, args);
Probably better to hide the casts in a macro that does some static type assertion to prevent it from casting things that aren’t subtypes of the method’s base type.
Another unsafe trick here is that offsetting a pointer to such value by usize produces a pointer to the struct value, nice.
Don Box’s book ‘Essential COM’ goes way further down this path for folks that are interested.
“Object oriented” code is just the combination of three other features: Dynamic dispatch, subtyping (inheritance), and namespacing. There’s no particular reason they should go together; it’s just easy to implement them with pointers and vtables in a way that gets you Smalltalk or Java. For example this implementation basically leaves out subtyping.