Understanding Dynamic Polymorphism in C++

cover
24 May 2024

Understanding Dynamic Polymorphism in C++

Dynamic polymorphism is a fundamental principle in object-oriented programming, allowing objects to be treated uniformly while exhibiting different behaviors. This feature is crucial for achieving flexibility and reusability in software design. In this article, we'll explore dynamic polymorphism with a sample program, explain heap and stack memory, delve into the virtual table (vtable), and discuss memory consumption details.

Achieving Dynamic Polymorphism in C++

To demonstrate dynamic polymorphism, consider the following example in C++:

#include <iostream>

// Base class
class Animal {
public:
    virtual void makeSound() const {
        std::cout << "Animal sound" << std::endl;
    }
    virtual ~Animal() = default; // Virtual destructor
};

// Derived class
class Dog : public Animal {
public:
    void makeSound() const override {
        std::cout << "Woof" << std::endl;
    }
};

// Another derived class
class Cat : public Animal {
public:
    void makeSound() const override {
        std::cout << "Meow" << std::endl;
    }
};

int main() {
    Animal* animal1 = new Dog();
    Animal* animal2 = new Cat();

    animal1->makeSound(); // Outputs: Woof
    animal2->makeSound(); // Outputs: Meow

    return 0;
}

In this example, Animal is the base class with a virtual method makeSound(). The Dog and Cat classes override this method. At runtime, the appropriate method is called based on the actual object type (Dog or Cat), demonstrating dynamic polymorphism.

C Variant

To understand how this is achieved in C, let's recreate the same program:

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

char const* dogSound(void){return "Woof!";}
char const* catSound(void){return "Meow!";}

typedef struct Animal{
    char const * (*makeSound)(); //function pointer
}Animal;

void makeSound(Animal * self){
    printf("%s\n",self->makeSound());
}

Animal * constructDog(){
    Animal * dog = malloc(sizeof(Animal));
    dog->makeSound = dogSound;
    return dog;
}

Animal * constructCat(){
    Animal * cat = malloc(sizeof(Animal));
    cat->makeSound = catSound;
    return cat;
}

int main(void){
    Animal * cat = constructCat(); 
    Animal * dog = constructDog();

    makeSound(cat); // Outputs: Woof
    makeSound(dog); // Outputs: Meow
    return 0;
}

Here, function pointers are used to achieve polymorphism in C. The Animal struct contains a function pointer makeSound, which is assigned appropriate functions (dogSound or catSound) at runtime.

Heap and Stack Explanation

In the sample program, objects animal1 and animal2 are created using the new keyword. This means they are allocated on the heap, which is a region of memory used for dynamic memory allocation. The delete keyword is used to deallocate memory, preventing memory leaks.

Conversely, if we had created objects without the new keyword (i.e., Dog dog;), they would be allocated on the stack. The stack is used for static memory allocation, which is automatically managed and has a smaller, faster access compared to the heap.

Vtable (Virtual Table)

The vtable (virtual table) is a mechanism used by C++ to support dynamic polymorphism. It is a table of function pointers maintained per class. Each class with virtual functions has a corresponding vtable, and each object has a pointer to this vtable, known as the vptr (virtual pointer).

When a virtual function is called on an object, the compiler uses the vptr to look up the correct function in the vtable and invoke it. This lookup allows C++ to resolve the function call at runtime, enabling dynamic polymorphism.

Memory Consumption

Memory consumption in dynamic polymorphism involves both the heap (for dynamic allocation) and the memory overhead of maintaining vtables and vptrs. Each object with virtual functions contains an additional pointer (vptr), and each class with virtual functions has an associated vtable.

While the heap allocation allows flexibility, it comes with a cost: dynamic memory management can lead to fragmentation, and improper deallocation can cause memory leaks. Additionally, the indirection through vtables can introduce a slight performance overhead due to the extra pointer dereference.

Conclusion

Dynamic polymorphism in C++ is a powerful feature that enables flexibility and reusability in object-oriented design. It is achieved through virtual functions, heap allocation, and the use of vtables. Understanding the memory implications and how the heap and stack work is essential for efficient programming in C++.

By following best practices in memory management and being mindful of the costs associated with dynamic polymorphism, developers can harness their full potential to create robust and maintainable software.