Using an array of Function Pointers for Scalability and Polymorphism in C
11/20/20Rationale
Often, the main complaint I have about my previously written code has to do with clarity and/or scalability. With embedded systems and tight timeframes, the quicker options and lighter footprints often prevail. Almost without exception, I know I'll be back to the same code to expand functionality. For this reason, thinking ahead about basic OOP principles always seems to pay off.
Structure
Generally, we try to keep encapsulation in C by local declaration of critical variables, exposing functional access in the header file. To use a standard trope, we can define the "shape" object as a struct. We use function pointers to point to the "shape" object's methods. The reason, I argue, to make these functions object methods, is that they will take on different functions depending on the shape. We can assign the function when we instantiate the instance of a shape.
//shape.h typedef struct SHAPE_T SHAPE_T; typedef float (*AREA_T)(SHAPE_T*); typedef float (*VOLUME_T)(SHAPE_T*); typedef struct SHAPE_T{ float height; float width; float radius; AREA_T getArea; VOLUME_T getVolume; }SHAPE_T; typedef enum { SHAPE_TYPE_CIRCLE, SHAPE_TYPE_TRIANGLE, SHAPE_TYPE_RECTANGLE }SHAPE_TYPE_T; SHAPE_T* shape_init(float height, float width, float radius, SHAPE_TYPE_T shapeType);
We forward declare the shape struct's methods for "area" and "volume." These are the object's "assignable" methods. For this example, let's use a circle, triangle, and rectangle to illustrate. We'll define them statically. Note that not all attributes will be used for a shape,
//shape.c #define PI (3.141592654f) float shape_areaCircle(SHAPE_T* this) { return 2*PI*this->radius; } float shape_areaTriangle(SHAPE_T* this) { return (this->width/2)*this->height; } float shape_areaRectangle(SHAPE_T* this) { return this->height * this->width; } AREA_T shape_area[] = { shape_areaCircle, shape_areaTriangle, shape_areaRectangle };
For each shape type we are interested in, we define a seperate "area" function to accomodate different formulas. Then an array of function pointers provides a clear ordering and selection based on an enumerated type. Opinions may begin to diverge here, but I appeal to clarity of process and ease of adding the next shape when I have to return a month later. Using typedef is a great way to clarify usage before the question arises.
I'm going to leave out the obvious "getters" and "setters" here, mainly just to argue the simple point of using function pointers as methods.
Initialization
I'm going to assuage my negative feelings about using the heap by forcing a block-size allocation. Also, I want to make sure that, as future additions to the struct template are added, we don't have to worry about it during allocation. However, I'm sure that if you are already doing this, you have a different function managing this. I'll keep this here as a reminder to look out for my future self.
#define HEAP_BLOCK_SIZE (32) #define MAX_HEAP_ALLOC (256) SHAPE_T* shape_heapAlloc(void) { size_t bytesToAllocate = 0; while(bytesToAllocate < HEAP_BLOCK_SIZE && bytesToAllocate <= MAX_HEAP_ALLOC) bytesToAllocate += HEAP_BLOCK_SIZE; if(sizeof(SHAPE_T) <= MAX_HEAP_ALLOC) return malloc(bytesToAllocate); else return NULL; } SHAPE_T* shape_init(float height, float width, float radius, SHAPE_TYPE_T shapeType) { SHAPE_T* s = shape_heapAlloc(); if(s != NULL) { s->height = height; s->width = width; s->radius = radius; s->getArea = shape_area[shapeType]; } return s; }
So we've defined each shape's "area" method in its constructor. The array of area functions is defined statically, and could carry the const modifier. But my concern here is just that we leave a clear plan for future expansion. To use the constructor and methods, we just declare some pointers and initialize them.
int main(int argc, char** argv) { //Initialize a rectangle of dimensions 3.6 x 4.5 SHAPE_T* s = shape_init(3.6,4.5,0,SHAPE_TYPE_RECTANGLE); if(s) float n = s->getArea(s); //Initialize a circle of radius 3.2 SHAPE_T* c = shape_init(0,0,3.2,SHAPE_TYPE_CIRCLE); if(c) n = c->getArea(c); free(s); free(c); return (EXIT_SUCCESS); }
This is obviously not the example that demonstrates the most useful case. However, if you put this in the framework of a situation that requires constant expansion and customization, I think it becomes helpful. An example might be the implementation of a series of display menus or sections of data that need to be parsed and updated on a display.