0
\$\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\$
  • 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 Feb 7 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 Feb 7 at 6:28
  • \$\begingroup\$ What is the goal of your program? What role do geometric shapes play in this goal? \$\endgroup\$ – Mast Feb 7 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 Feb 7 at 6:29
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\$
  • \$\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 Feb 6 at 16:02

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.