1
\$\begingroup\$

In the following code I have created the something like the behavior of inheritance and methods in C99 (without vtable). The code compiles without any warnings even with pedantic and is also Valgrind Pristine. I would like to hear about design patterns and techniques to improve code quality, readability and portability to major compilers (as gcc, clang, vsc and icc).

The code is divided to "simulate" multiple files, the first part defines some basic struct and functions.

Second part defines the parent polygon class.

Third part defines a polygon inherited class called triangle.

Fourth part defines a square from polygon mostly defined using function placeholders.

And the fifth part defines the main program that uses the structs defined before for simple calls and changes in struct behaviors.

#include <stdio.h> #include <stdlib.h> #include <math.h> #include <string.h> #include <stdbool.h> /* ************************************************** * The basics * **************************************************/ struct point { float x; float y; }; static inline float distance (struct point p1, struct point p2); // a.k.a. l2 norm static inline float distance (struct point p1, struct point p2) { return sqrt ((p1.x - p2.x) * (p1.x - p2.x) + (p1.y - p2.y) * (p1.y - p2.y)); } /* ************************************************** * POLYGON * **************************************************/ /* Header */ struct polygon { int sides; // All polygons have sides struct point vertex[20]; // No more than 20 sided polygon void (*destroy) (); // Unknown parameters float (*area) (); // Unknown parameters float (*perimeter) (); // Unknown parameters void (*action) (); // A polygon also needs some action }; float null_area (); float null_perimeter (); void null_action (); float generic_area (struct polygon *p); float generic_perimeter (struct polygon *p); void generic_action (void); struct polygon *polygon_init (int sides); void polygon_destroy (struct polygon *p); float polygon_area (struct polygon *poly); float polygon_perimeter (struct polygon *p); void polygon_action (struct polygon *p); bool all_sides_are_congruent (struct polygon *p); /* NULL function */ float null_area (void) { return -1; } float null_perimeter (void) { return -1; } void null_action (void) { return; } float generic_area (struct polygon *poly) { return null_area (); // Not implemented } float generic_perimeter (struct polygon *poly) { float p = 0; for (int i = 0; i < poly->sides - 1; i++) p += distance (poly->vertex[i], poly->vertex[i + 1]); return p + distance (poly->vertex[poly->sides - 1], poly->vertex[0]); } void generic_action (void) { printf ("I'm idle!\n"); // Lazy Polygon - not much action } /* Specialized functions */ struct polygon * polygon_init (int sides) { struct polygon *poly; poly = malloc (sizeof *poly); poly->sides = sides; poly->perimeter = generic_perimeter; poly->area = generic_area; poly->action = generic_action; poly->destroy = free; // Like a generic destroy ;-) return poly; } void polygon_destroy (struct polygon *poly) { poly->destroy (poly); } float polygon_perimeter (struct polygon *poly) { return poly->perimeter (poly); } float polygon_area (struct polygon *poly) { return poly->area (poly); } void polygon_action (struct polygon *poly) { printf ("Poly addr %p: ", (void *) poly); poly->action (); } bool all_sides_are_congruent (struct polygon *p) { int i = 0; float t = 0.; float s = 0.; s = distance (p->vertex[p->sides - 1], p->vertex[0]); for (i = 0; i < p->sides - 1; i++) { t = distance (p->vertex[i], p->vertex[i + 1]); if (s != t) { return false; } } return true; } /* ************************************************** * TRIANGLE * **************************************************/ /* HEADER */ struct triangle { // Triangles are like polygons, but for sake of this example // they have heights (shhh. all polygons have heights) struct polygon polygon; float (*height) (); }; // Not triangle struct dependant float heron_area_formula (float a, float b, float c); float shoelace_area_formula (struct point p0, struct point p1, struct point p2); /* Triangle struct dependant functions */ struct triangle *triangle_init (void); /* First ones from polygon */ void triangle_destroy (struct triangle *tri); float triangle_area (struct triangle *tri); float triangle_perimeter (struct triangle *tri); void triangle_action (struct triangle *tri); /* More specilized ones */ /* These two extract the needed information to the non triangle dependant * functions */ float triangle_heron_area (struct triangle *tri); float triangle_shoelace_area (struct triangle *tri); /* These are specilized to triangle struct only, not existing in general * polygons */ float triangle_height (struct triangle *tri); float triangle_height_from_base1 (struct triangle *tri); float triangle_height_from_base2 (struct triangle *tri); float triangle_height_from_base3 (struct triangle *tri); /* Functions */ /* Some known formulas to calculate triangle area */ // Calculates triangle area given face three distances float heron_area_formula (float a, float b, float c) { float p = (a + b + c) / 2; return sqrt (p * (p - a) * (p - b) * (p - c)); } // Calculates triangle area given three vertices float shoelace_area_formula (struct point p0, struct point p1, struct point p2) { return .5 * fabs (p0.x * p1.y - p0.y * p1.x + p1.x * p2.y - p1.y * p2.x + p2.x * p0.y - p2.y * p0.x); } struct triangle * triangle_init (void) { struct triangle *tri; tri = malloc (sizeof *tri); tri->polygon.sides = 3; tri->polygon.destroy = free; tri->polygon.action = generic_action; tri->polygon.perimeter = generic_perimeter; tri->polygon.area = triangle_heron_area; // By default we use heron's formula tri->height = triangle_height_from_base1; // By default height is related to first 2 points return tri; } void triangle_destroy (struct triangle *tri) { tri->polygon.destroy (tri); } float triangle_heron_area (struct triangle *tri) { return heron_area_formula (distance (tri->polygon.vertex[0], tri->polygon.vertex[1]), distance (tri->polygon.vertex[1], tri->polygon.vertex[2]), distance (tri->polygon.vertex[2], tri->polygon.vertex[0])); } float triangle_shoelace_area (struct triangle *tri) { return shoelace_area_formula (tri->polygon.vertex[0], tri->polygon.vertex[1], tri->polygon.vertex[2]); } float triangle_area (struct triangle *tri) { return tri->polygon.area (tri); } float triangle_perimeter (struct triangle *tri) { return tri->polygon.perimeter ((struct polygon *) tri); } void triangle_action (struct triangle *tri) { printf ("Triangle addr %p: ", (void *) tri); printf ("Triple action\n"); tri->polygon.action (); tri->polygon.action (); tri->polygon.action (); } // Height from three bases float triangle_height_from_base1 (struct triangle *tri) { return 2 * triangle_area (tri) / distance (tri->polygon.vertex[0], tri->polygon.vertex[1]); } float triangle_height_from_base2 (struct triangle *tri) { return 2 * triangle_area (tri) / distance (tri->polygon.vertex[1], tri->polygon.vertex[2]); } float triangle_height_from_base3 (struct triangle *tri) { return 2 * triangle_area (tri) / distance (tri->polygon.vertex[2], tri->polygon.vertex[0]); } // Pick one float triangle_height (struct triangle *tri) { return tri->height (tri); } /* ************************************************** * SQUARE * **************************************************/ struct square { struct polygon polygon; float (*height) (); bool (*is_square) (struct square * s); }; struct square *square_init (void); void square_destroy (struct square *s); float square_area (struct square *s); float square_perimeter (struct square *s); void square_action (struct square *s, int num); float square_height (struct square *s); bool square_check (struct square *s); bool square_check_by_side (struct square *s); float height_from_side (struct square *s); struct square * square_init (void) { struct square *s; s = malloc (sizeof *s); s->polygon.sides = 4; s->polygon.destroy = free; s->polygon.area = null_area; s->polygon.perimeter = null_perimeter; s->polygon.action = null_action; s->is_square = square_check_by_side; s->height = height_from_side; return s; } void square_destroy (struct square *s) { s->polygon.destroy (s); } float square_area (struct square *s) { return s->polygon.area (s); } float square_perimeter (struct square *s) { return s->polygon.perimeter (s); } void square_action (struct square *s, int num) { s->polygon.action (s); } float square_height (struct square *s) { return s->height (s); } bool square_check_by_side (struct square * s) { if ((s->polygon.sides == 4) && all_sides_are_congruent (&(s->polygon))) { return true; } else { return false; } } float height_from_side (struct square *s) { return distance (s->polygon.vertex[0], s->polygon.vertex[1]); } bool square_check (struct square * s) { return s->is_square (s); } /* ************************************************** * MAIN CODE * **************************************************/ int main (void) { // Method 1 struct polygon p1; p1.sides = 3; p1.vertex[0] = (struct point) { .x = 0,.y = 0}; p1.vertex[1] = (struct point) { .5, 1}; p1.vertex[2].x = 1; p1.vertex[2].y = 0; p1.perimeter = &generic_perimeter; p1.action = &generic_action; printf ("Perimeter of P1 %.2f\n", polygon_perimeter (&p1)); polygon_action (&p1); printf ("\n"); // Method 2 struct polygon *p2 = polygon_init (3); struct point t[3] = { {0, 0}, {.5, 1}, {1, 0} }; memcpy (p2->vertex, t, sizeof (t)); printf ("Perimeter of P2 %.2f\n", polygon_perimeter (p2)); polygon_action (p2); polygon_destroy (p2); printf ("\n"); // A triangle struct triangle *t1 = triangle_init (); memcpy (t1->polygon.vertex, t, sizeof (t)); printf ("Perimeter of T1 %.2f\n", triangle_perimeter (t1)); printf ("Area of T1 %.2f\n", triangle_area (t1)); t1->polygon.area = triangle_shoelace_area; // Let's use another area formula printf ("Area of T1 %.2f\n", triangle_area (t1)); printf ("Height of T1 base 1 %.2f\n", triangle_height (t1)); t1->height = triangle_height_from_base2; printf ("Height of T1 base 2 %.2f\n", triangle_height (t1)); t1->height = triangle_height_from_base3; printf ("Height of T1 base 3 %.2f\n", triangle_height (t1)); triangle_action (t1); triangle_destroy (t1); printf ("\n"); // Now a square; struct square *s1 = square_init (); struct point s[4] = { {0, 0}, {0, 1}, {1, 1}, {1, 0} }; memcpy (s1->polygon.vertex, s, sizeof (s)); printf ("Perimeter of S1 %.2f\n", square_perimeter (s1)); printf ("Area of S1 %.2f\n", square_area (s1)); printf ("Height of S1 %.2f\n", square_height (s1)); // Why conditional jump here? printf ("Is S1 a square? %s\n", (square_check (s1) ? "true" : "false")); printf ("Square action\n"); square_action (s1, 5); square_destroy (s1); return 0; } ```
\$\endgroup\$
4
  • 1
    \$\begingroup\$Honest question: why are you restricted to a version of C that's 20 years old when thanks to internet access and a massive open-source community there are plenty of free, modern compilers available that will handle newer versions just fine?\$\endgroup\$
    – Mast
    CommentedFeb 7, 2019 at 6:25
  • \$\begingroup\$You say the code is divided to simulate multiple files. What is your actual situation? Do you have multiple files or not? Why do you feel the need to simulate?\$\endgroup\$
    – Mast
    CommentedFeb 7, 2019 at 6:28
  • \$\begingroup\$What is the goal of your program? What role do geometric shapes play in this goal?\$\endgroup\$
    – Mast
    CommentedFeb 7, 2019 at 6:28
  • 2
    \$\begingroup\$The current question title, which states your concerns about the code, applies to too many questions on this site to be useful. The site standard is for the title to simply state the task accomplished by the code. Please see How to Ask for examples, and revise the title accordingly.\$\endgroup\$
    – Mast
    CommentedFeb 7, 2019 at 6:29

1 Answer 1

5
\$\begingroup\$

First of all, the obvious remark is that this needs to be split in multiple files, with a .h/.c pair for each class. Since you haven't done so, you block the possibility to make this truly OO.

It is quite cumbersome in C, but can be done. As it happens, OO lies very close to old school proper C program design with ADTs, from the time before OO was invented. A rose by any other name...

The review below focuses on OO design in C only.


Inheritance and polymorphism in C, as well as private encapsulation, is done through the concept of opaque types/opaque pointers. Meaning that you have a header like this:

// polygon.h typedef struct polygon polygon; polygon* poly_create (void); ... 

This defines an incomplete type that the caller can't access directly, nor allocate. (Much like an abstract base class in C++.)

The C file will then be along the lines of:

// polygon.c #include "polygon.h" struct polygon { /* member variables here */ }; polygon* poly_create (void) { polygon* obj = malloc(sizeof *obj); ... return obj; } 

This achieves true private encapsulation but not inheritance, because the struct definition will not be available to neither the caller nor anyone who wants to inherit (it is private rather than protected if you will).


To achieve inheritance, polymorphism and inheritance restrictions, we need to expose the part of the struct that inherited classes may access.

The file structure then becomes:

// polygon.h typedef struct polygon polygon; typedef struct polygon_private polygon_private; typedef void function_to_inherit (void); polygon* poly_create (void); 

where polygon_private is another incomplete type that will contain everything truly private and not inheritable.

function_to_inherit is a function type which we will use for polymorphism.

Then add another header visible to the inherited class but not the caller, containing the struct implementation:

// polygon_inherit.h #include "polygon.h" struct polygon { polygon_private* priv; // incomplete type, inaccessible from here function_to_inherit* do_stuff; // function pointer that allows for polymorphism /* other protected members here */ }; 

The definition of the private ends up in polygon.c only:

// polygon.c #include "polygon.h" #include "polygon_inherit.h" struct polygon_private { /* private stuff */ }; polygon* poly_create (void) { polygon* obj = malloc(sizeof *obj); ... obj->priv = malloc (sizeof *obj->priv); obj->priv->secret_stuff = ...; ... obj->do_stuff = print_polygon; ... return obj; } 

And then we may inherit this:

// triangle.h #include "polygon.h" #include "polygon_inherit.h" typedef struct triangle triangle; triangle* triangle_create (void); 

With the implementation:

// triangle.c #include "triangle.h" struct triangle { polygon* parent; /* triangle-specific stuff here */ }; triangle* triangle_create (void) { triangle* obj = malloc(sizeof *obj); obj->parent = polygon_create(); // thanks to polygon_inherit.h, we have access to the parent's protected members: obj->parent->do_stuff = print_triangle; // polymorphism } 

All inherited functions need to call those in the base class where applicable.

I didn't write any destructors but of course those too need to be added in similar fashion. And since it is C, all constructors/destructors need to be called manually, since there's no RAII.

\$\endgroup\$
1
  • 1
    \$\begingroup\$Regardless of programming language, OO features like private encapsulation and autonomous objects are universally good features that should always be used. Inheritance/polymorphism, not so much. It should only be used where it truly makes sense - not spewed all over the program for the sake of it. The problem with inheritance being that at the point when we write the base class, we can rarely ever foresee all future needs of the inherited classes. And so we risk writing burdensome interfaces that do more harm than good. Properly written inheritance always allows re-design of the base class.\$\endgroup\$
    – Lundin
    CommentedFeb 6, 2019 at 16:02

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.