A Practical Guide to Dynamic Polymorphism in C Programming

Written by savicnikola | Published 2024/05/26
Tech Story Tags: c-programming | dynamic-polymorphism | embedded-systems | design-patterns | pointers-in-c | code-flexibility | modular-programming | programming-paradigms

TLDRDynamic polymorphism allows objects to be treated uniformly while exhibiting different behaviors, enhancing code flexibility and maintainability. This article compares two C implementations of a toy simulation, demonstrating the benefits of dynamic polymorphism through function pointers and factory functions.via the TL;DR App

Dynamic polymorphism is a programming paradigm that enhances code flexibility and maintainability by allowing objects to be treated uniformly while exhibiting different behaviors. This concept is particularly useful in scenarios involving collections of related objects that need to perform specific actions. In this article, we compare two implementations of a toy simulation in C to demonstrate the benefits of dynamic polymorphism

Initial Implementation without Polymorphism

Here we define a Toy struct with a name field and create three instances representing Barbie and Superman toys. Each toy is initialized, and an array of pointers to these toy instances is created. The main function iterates through the array and prints the sound associated with each toy based on its name.

#include <stdio.h>
#include <stdlib.h>

// Function prototypes for the sounds made by different toys
char const* barbieSound(void){return "Love and imagination can change the world.";}
char const* supermanSound(void){return "Up, up, and away!";}

// Struct definition for Toy with an character array representing the name of the toy
typedef struct Toy{
    char const* name;  
}Toy;

int main(void){
    //Alocate memory for three Toy instances
    Toy* toy1 = (Toy*)malloc(sizeof(Toy));
    Toy* toy2= (Toy*)malloc(sizeof(Toy));
    Toy* toy3= (Toy*)malloc(sizeof(Toy));

    // Initialize the name members of the Toy instances
    toy1->name = "Barbie";
    toy2->name = "Barbie";
    toy3->name = "Superman";
    
    // Create an array of pointers to the Toy instances
    Toy* toys[] = {barbie1, barbie2, superman1};
    
    // Output the corresponding sound of each toy given its name
    for(int i=0; i < 3; i++){
        if (toys[i]->name == "Barbie"){printf("%s\n",toys[i]->name,barbieSound());}
        if (toys[i]->name == "SuperMan"){printf("%s\n",toys[i]->name,supermanSound());}
    }

    // Free the allocated memory for the Toy instances
    free(toy1);
    free(toy2);
    free(toy3);
    
    return 0;
}

While this is functional, it doesn't scale. Whenever we want to add a new toy we need to update the code to handle a new toy type and its sound function which could raise maintenance issues.

Enhanced Implementation of Dynamic Polymorphism:

The second code sample uses dynamic polymorphism for a more flexible, scalable application. Here Toy has its function pointer for the sound function. Factory functions(createBarbie(), createSuperMan()) are used to create Barbie and Superman instances, assigning the appropriate sound function to each toy. The makeSound() function demonstrates dynamic polymorphism by calling the appropriate sound function for each toy at run time.

#include <stdio.h>
#include <stdlib.h>

// Function prototypes for the sounds made by different toys
char const* barbieSound(void) {return "Love and imagination can change the world.";}
char const* supermanSound(void) {return "I’m here to fight for truth and justice, and the American way.";}

// Struct definition for Toy with a function pointer for the sound function
typedef struct Toy {
    char const* (*makeSound)();
} Toy;

// Function to call the sound function of a Toy and print the result
// Demonstrates dynamic polymorphism by calling the appropriate function for each toy
void makeSound(Toy* self) {
    printf("%s\n", self->makeSound());
}

// Function to create a Superman toy
// Uses dynamic polymorphism by assigning the appropriate sound function to the function pointer
Toy* createSuperMan() {
    Toy* superman = (Toy*)malloc(sizeof(Toy));
    superman->makeSound = supermanSound; // Assigns Superman's sound function
    return superman;
}

// Function to create a Barbie toy
// Uses dynamic polymorphism by assigning the appropriate sound function to the function pointer
Toy* createBarbie() {
    Toy* barbie = (Toy*)malloc(sizeof(Toy)); 
    barbie->makeSound = barbieSound; // Assigns Barbie's sound function
    return barbie;
}

int main(void) {
    // Create toy instances using factory functions
    Toy* barbie1 = createBarbie();
    Toy* barbie2 = createBarbie();
    Toy* superman1 = createSuperMan();
    
    // Array of toy pointers
    Toy* toys[] = { barbie1, barbie2, superman1 };
    
    // Loop through the toys and make them sound
    // Dynamic polymorphism allows us to treat all toys uniformly
    // without needing to know their specific types
    for (int i = 0; i < 3; i++) {
        makeSound(toys[i]);
    }
    
    // Free allocated memory
    free(barbie1);
    free(barbie2);
    free(superman1);

    return 0;
}

By using a function pointer within the Toy struct, the code can easily accommodate new toy types without modifying the core logic.

Factory function is a design pattern used to create objects without specifying the exact class or type of the object that will be created. In the context of C programming and especially in embedded software, a factory function helps in managing the creation and initialization of various types of objects (e.g., sensors, peripherals) in a modular and flexible manner. This pattern is particularly useful when dealing with dynamic polymorphism, as it abstracts the instantiation logic and allows the system to treat objects uniformly

A function pointer is a variable that stores the address of a function, allowing the function to be called through the pointer. This enables dynamic function calls, which are particularly useful when the function to be executed needs to be determined at runtime.

Defining a Function Pointer

A function pointer is defined using the following syntax:

<return type> (*<pointer name>)(<parameter types>);

Example

For example, to declare a pointer to a function that returns an int and takes two int parameters, you would write:

int (*functionPointer)(int, int);


Written by savicnikola | Software engineer with experience in C++ and machine learning with elixir
Published by HackerNoon on 2024/05/26