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
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.
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.
A function pointer is defined using the following syntax:
<return type> (*<pointer name>)(<parameter types>);
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);
Memory Overhead: Each structure that uses function pointers requires additional memory to store these pointers. This can slightly increase your program's memory footprint, especially if many such structures are used.
Function Call Overhead: Indirect function calls through pointers can introduce a slight performance overhead compared to direct function calls. This is due to the additional level of indirection required to resolve the function pointer at runtime.
When a function is called directly, the assembly code typically contains a direct jump to the function's address. For example, let’s take a simple C program:
#include <stdio.h>
void function(){
printf("Hello Barbie");
}
int main(void){
function();
void (*myFunction)() = function;
myFunction();
return 0;
}
The assembly code of the C code compiled with an x86-64 GCC 14.1 compiler looks like this:
.LC0:
.string "Hello Barbie"
function:
push rbp
mov rbp, rsp
mov edi, OFFSET FLAT:.LC0
mov eax, 0
call printf
nop
pop rbp
ret
main:
push rbp # Save the base pointer of the previous stack frame by pushing it on the stack
mov rbp, rsp # Set up a new stack frame for the current function
mov eax, 0 # Clear the eax register
call function # Call the function 'function'
mov eax, 0 # Set eax register to 0, indicating the return value of the main function
pop rbp # Pop the saved value from stack into rbp, restoring the previous stack frame
ret # Return from main function
We can see that in this case, calling the function takes only one instruction in the assembly code (call function
).
If we add a function pointer called myFunction
to point to the function:
void (*myFunction)() = function;
myFunction();
The assembly code of the main function will look like this:
main:
push rbp
mov rbp, rsp
sub rsp, 16 # Allocate 16 bytes on the stack for local variables
mov eax, 0
call function
mov QWORD PTR [rbp-8], OFFSET FLAT:function # Store the address of function into the memory location [rbp-8]
mov rdx, QWORD PTR [rbp-8] # Load the function pointer stored at [rbp-8] into rdx register
mov eax, 0 # Clear the eax register before function call, beacause the function might use it
call rdx # Call the function stored in rdx
mov eax, 0 # Set the return value of main to 0
leave # Release the stack space used by the current stack frame
ret
We can see that when using a function pointer, we have one additional assembly instruction for storing the function address in the function pointer and three additional instructions for calling the function pointed to by the function pointer. Also, 16 bytes had to be allocated for storing local variables, while only 8 bytes of the stack memory were used by the function pointer. This is because the stack size typically needs to be a multiple of 16, so if we were to use 20 bytes in our function, the compiler would allocate 32 bytes for the stack frame.
Dynamic polymorphism is often used in various software design patterns, such as the Strategy Pattern, State Pattern, and Command Pattern. These patterns make use of polymorphism to allow different behaviors and states to be easily swapped in and out. This makes the code more flexible and easier to maintain.