For non-trivial class hierarchies the method proposed in Part2 is probably not optimal. The main reason is that there is only one C struct which is used by base and subclasses. Consequently, the hierarchical tree is only implicitly contained in the data structures and therefor, in this last part of the series, we introduce a more explicit technique that closely resembles “C++ in C”.
Let us start with main() just to show where we’re heading for. The UML diagram of the code can be found in Part2 except that we have changed ‘id’ to ‘label’.
#include "shape.h" #include <stdio.h> int main(int argc, char ** argv) { shapePtr s = circle_alloc("my_circle", 10); printf("label: %s\n", s->vtable->get_label(s)); printf("area: %d\n", s->vtable->get_area(s)); s->vtable->free(s); return 0; }
Only at allocation the type (circle in this case) needs to be specified. Thereafter, all operations are executed by means of a virtual function table (vtable).
This brings us to the definition of a shape.
#ifndef SHAPE_H #define SHAPE_H typedef struct _shape* shapePtr; typedef struct _shape_vtable { const char* (*get_label)(const shapePtr); int (*get_area)(const shapePtr); void (*free)(shapePtr); } shape_vtable; typedef struct _shape_data { const char* label; } shape_data; typedef struct _shape { const shape_vtable* vtable; shape_data data; } shape; shapePtr circle_alloc(const char *label, int radius); #endif
A shape is composed of:
- shape_vtable, and
- shape_data.
A subclass needs to implement and possibly extend the vtable. Shape_data is always re-used and might be extended as well. Here is the definition of circle:
#ifndef CIRCLE_H #define CIRCLE_H #include "shape-p.h" typedef struct _circle_vtable { shape_vtable base_vtable; } circle_vtable; typedef struct _circle_data { shape_data base_data; int radius; } circle_data; typedef struct _circle { const circle_vtable* vtable; circle_data data; } circle, *circlePtr; #endif
In this particular case, shape’s vtable is not extended but shape_data is re-used and one data property radius is added.
Finally, the implementation of circle is as follows:
#include <math.h> #include <string.h> #include "circle.h" static void circle_free(shapePtr s); static int circle_get_area(const shapePtr s); static const circle_vtable circle_vtable_imp = { { .free = circle_free, .get_label = shape_get_label, .get_area = circle_get_area } }; static void circle_free(shapePtr s) { shape_free(s); } static int circle_get_area(const shapePtr s) { circlePtr c = (circlePtr) s; return c->data.radius * c->data.radius * M_PI; } shapePtr circle_alloc(const char *label, int radius) { circlePtr c = (circlePtr) malloc(sizeof(circle)); shape_init((shapePtr) c, (shape_vtable*) &circle_vtable_imp, label); c->data.radius = radius; return (shapePtr) c; }
The function circle_alloc mallocs a circle object and initializes itself and its parent class. However, it returns a shape (and not circle) pointer! Because circle_vtable_imp is a const pointer:
- all circle objects (class instances) can re-use the same vtable implementation, and
- it saves some RAM in XIP (execute-in-place) embedded systems.
The shape implementation is left to the reader.
With this method the class hierarchy is very clear. On the other hand, it is more verbose (LOC) than the solution proposed in Part2. Therefor, which method to choose depends on use case and personal preference.
Speak Your Mind