[blog] Runtime polymorphism exploited

In this post I will be showing a dynamic polymorphism implemented in C to take a look on what C++ implements under the hood.

The idea is to have an abstract class Polygon which has a public method to calculate the area of a given general polygon.

In C++ the implementation is straightforward.

#include <iostream>
class Polygon {
public:
    virtual uint32_t area() = 0;
};
class Triangle : public Polygon {
public:
    Triangle(uint32_t height, uint32_t base, const char* name)
    {
        this->height = height;
        this->base = base;
        this->name = name;
    }
  
private:
    uint32_t height;
    uint32_t base;
    const char* name;
    uint32_t area() {
        printf("Identified %s as a triangle.\n\r", name);
        return ((this->height) * (this->base) / 2);
    }
};
class Rectangle : public Polygon {
public:
    Rectangle(uint32_t height, uint32_t base, const char* name)
    {
        this->height = height;
        this->base = base;
        this->name = name;
    }
  
private:
    uint32_t height;
    uint32_t base;
    const char* name;
    uint32_t area() {
        printf("Identified %s as a rectangle.\n\r", name);
        return ((this->height) * (this->base));
    }    
};
int main()
{
    Polygon* p1;
    Polygon* p2;
    Triangle t1{ 5,6, "polygon1"};
    Rectangle r1{ 9,5, "polygon2"};
    p1 = &t1;
    p2 = &r1;
    printf("area: %d\n\r", p1->area());
    printf("area: %d\n\r", p2->area());
}
C++ does not have an Interface keyword. You have to define interfaces through inheritance mechanisms, as we do to inherit members. To declare an interface, declare a pure virtual class. To implement an interface, derive from it.

Output

Identified polygon1 as a triangle.
area: 15
Identified polygon2 as a rectangle.
area: 45

The implementation in C can be done too. We need to know how to handle the memory structures to create the late-bind. An abstract class as “Polygon” in C , or the base class, can be seen as a pointer to a structure that is on the top (the lowest addresss) of the memory structure of every derived class. It means that a given polygon object, let it be a square or a hexagon will always be bound by this interface. But we have to wire them so they are useful together.

Note that the derived class is inheriting an i-n-t-e-r-f-a-c-e. There is a lot in common between different polygons and in how we manipulate them, so we can easily think of such an intefarce. Note that, in fact a triangle is a Polygon. Not every polygon is a triangle though. That's why we loosely say abstract classes can't be instantiated. 
UML class diagram and conceptual memory layout. This Polygon is composed as a rectangle.
//@File: polygon.h
#ifndef POLYGON_H 
#define POLYGON_H
#include <stdint.h>
struct VirtualTable;
typedef struct
{
	struct VirtualTable const* vptr;
} Polygon_t;
struct VirtualTable 
{
	uint32_t (*CalcArea)(Polygon_t const* const me);
};
void Polygon_Ctor(Polygon_t* const me);
//inline to save a function call
static inline uint32_t CalcArea(Polygon_t const* const me)
{
	return (*me->vptr->CalcArea)(me);
}
#endif
//@File: rectangle.h
#ifndef RECTANGLE_H
#define RECTANGLE_H
#include "polygon.h"
typedef struct
{
	Polygon_t Super; //<-Base Class
	uint32_t base;
	uint32_t height;
    const char* name;
} Rectangle_t;
void Rectangle_Ctor(Rectangle_t* this_rectangle, int base, int height, const char* name);
#endif
//@File: triangle.h
#ifndef TRIANGLE_H
#define TRIANGLE_H
#include "polygon.h"
typedef struct
{
	Polygon_t Super;
	int base;
	int height;
    const char* name;
} Triangle_t;
void Triangle_Ctor(Triangle_t* this_triangle, int base, int height, const char* name);
#endif
//@File polygon.c
#include "polygon.h"
#include <assert.h>
#include <stdlib.h>
static uint32_t CalcArea_(Polygon_t const* const me);
void Polygon_Ctor(Polygon_t* const this_polygon) 
{
	if (this_polygon != NULL) {
		static struct VirtualTable const vtbl =
		{
			&CalcArea_
		};
		this_polygon->vptr = &vtbl;
	}
}
static uint32_t CalcArea_(Polygon_t const* const me)
{
	assert(0); //should never be called
	return 0;
}

The base-class (or the Super class) has a pointer vptr to a VirtualTable structure. In this case this virtual table has a single function pointer. When called it relies on the caller to tell which function it should point to, what should be done. That is, depending on which derived class calls this function, it will perform a different computation. When constructing the derived object you bind an interface address to the object’s own base class’s virtual table. Before you have to construct the interface within the object.

//@File: triangle.c
#include <stdlib.h>
#include "polygon.h"
#include "triangle.h"
static uint32_t CalcAreaTriang_(Polygon_t const* const this_polygon);
void Triangle_Ctor(Triangle_t* this_triangle, int base, int height, const char* name)
{
	static struct VirtualTable const vtbl =
	{
		&CalcAreaTriang_ 
	};
	if (this_triangle != NULL)
	{
     //constructing the base-class
		Polygon_Ctor(&this_triangle->Super); 
     //binding this vtable to base-class table
		this_triangle->Super.vptr = &vtbl; 
    //defining class members
		this_triangle->base = base;
		this_triangle->height = height;
		this_triangle->name = name;
	}
}
static uint32_t CalcAreaTriang_(Polygon_t const* const me)
{
	Triangle_t* this_triangle = (Triangle_t*)me; //downcast
	printf("Identified %s as a triangle.\n\r", this_triangle->name);
	return ((this_triangle->height) * (this_triangle->base))/2;
}
//@File: rectangle.c
#include "polygon.h"
#include "rectangle.h"
#include <stdint.h>
#include <stdlib.h>
#include <stdio.h>
static uint32_t CalcAreaRect_(Polygon_t const* const this_polygon);
void Rectangle_Ctor(Rectangle_t* this_rectangle, int base, int height, const char* name)
{
	static struct VirtualTable const vtbl = 
	{
		&CalcAreaRect_
	};
	if (this_rectangle != NULL) 
	{
		Polygon_Ctor(&this_rectangle->Super);
		this_rectangle->Super.vptr = &vtbl;
		this_rectangle->base = base;
		this_rectangle->height = height;
		this_rectangle->name = name;
	}
}
static uint32_t CalcAreaRect_(Polygon_t const* const me)
{
	Rectangle_t* this_rectangle = (Rectangle_t*)me;
	printf("Identified %s as a rectangle.\n\r", this_rectangle->name);
	return (this_rectangle->base) * (this_rectangle->height);
}

Since the private function which calculates the area receives a pointer to Polygon_t, we perform a downcast (a cast from base class to a derived class, Polygon_t to Triangle_t, e.g.) to access the data we need.

To use the CalcArea method we either perform an upcast on the object (e.g., from Triangle_t to Polygon_t) or access the Super class instance through the object address.

#include <stdio.h>
#include <stdlib.h>
#include "polygon.h"
#include "rectangle.h"
#include "triangle.h"
int main()
{
    //declaring objects
    Rectangle_t r1;
    Triangle_t t1;
    Rectangle_t r2 ;
    //constructing objects
    Rectangle_Ctor(&r1, 10, 20, "Polygon1");
    Triangle_Ctor(&t1, 5, 10, "Polygon2");
    Rectangle_Ctor(&r2, 300, 2, "Polygon3");
    //computing areas polymorphically
    printf("area: %d\n\r", CalcArea(&r1.Super));
    printf("area: %d\n\r", CalcArea((Polygon_t*)&t1););
    printf("area: %d\n\r", CalcArea(&r2.Super));
}
Identified Polygon1 as a rectangle.
area: 200
Identified Polygon2 as a triangle.
area: 25
Identified Polygon3 as a rectangle.
area: 600

Author: Antonio Giacomelli de Oliveira

Embedded Systems Engineer

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: