Page Content

Tutorials

C++ Polymorphism: Core Object Oriented Programming Concept

C++ Polymorphism

Object-Oriented Programming (OOP) uses polymorphism (Greek for “many forms”) to describe a wide range of operations with a single name or interface. The kind of data involved then dictates the particular behaviour. Polymorphism in C++ allows related objects to be regarded as though they behave similarly while yet allowing the system to call the distinct methods of each object. C++ supports static and dynamic polymorphism.

Dynamic polymorphism, also known as run-time polymorphism or late binding, decides which function to employ while the program is executing rather than compiling. The main tools used to accomplish this are virtual functions and base class references or pointers. The basic tenet is that types that are connected by inheritance can be regarded as polymorphic types, employing their “many forms” while disregarding their particular distinctions. C++’s support for dynamic polymorphism is based on the ability of references and pointers to have different static types (known at compile time) and dynamic types (the actual object type at run time).

Key Components of Dynamic Polymorphism

Virtual Functions: The virtual keyword in a base class member function allows descendent classes to redefine it. The virtual keyword allows the function to be called at runtime depending on the object type by asking the compiler not to use static linkage (compile-time resolution). Virtual functions are an example of the “one interface, multiple methods” principle, in which the form of the interface is defined by the base class and its particular implementation is provided by each derived class.

When deleting derived objects using a base class pointer, a virtual destructor is necessary for polymorphic base classes to guarantee correct cleanup. In derived classes, a function’s intention to override a base class virtual function can be explicitly indicated by using the override specifier.

Base Class Pointers and References: Use a base class pointer or reference to invoke a virtual function for dynamic polymorphism. Even if the pointer or reference is of the base class type (static type), the proper virtual function is selected at runtime based on its dynamic type.

Virtual Method Tables (VMT): Internally, Virtual Method Tables permit dynamic binding. Each class with at least one virtual method has a VMT constructed for it, which contains an array of the virtual method addresses. Polymorphic classes have concealed VMT pointers in all objects. The program reads the VMT pointer in the object being referred to and calls the appropriate virtual function address from the VMT.

Pure Virtual Functions and Abstract Classes: Add = 0 to the base class declaration of a virtual function to make it “pure” (virtual void func() = 0;, for example). An abstract class is one that has one or more functions that are pure virtual. Direct instances (objects) of abstract classes cannot be made. Interfaces are frequently defined by abstract classes and provide a contract of behaviour that concrete derived classes must follow. By acting as polymorphic interfaces, they guarantee that every derived class offers an implementation of the specified behaviour.

Object Slicing: Polymorphism may have problems with object slicing. When an object of the base type is initialised or assigned using an object of a derived type, this happens. The derived-class data and behaviour are lost in this case when the derived-specific part of the object is “sliced down,” and only the base part is allocated to the base object. Because dynamic polymorphism preserves the object’s entire dynamic type, it helps avoid object slicing. This is especially true when pointers and references are used.

Run-Time Type Identification (RTTI): For base-class pointers to polymorphic types, the dynamic_cast and typeid operators provide Run-Time Type Identification (RTTI), which helps a programme determine an object’s type as it executes. Dynamic_cast throws bad_cast for references and nullptr for pointers if a verified conversion between related classes fails at runtime. Typeid offers details on an object’s type, including its name. Virtual functions are often recommended above typeid or dynamic_cast for type-based discrimination, notwithstanding their usefulness.

Code Examples

Code Examples Illustrating Dynamic Polymorphism

Shape Hierarchy Example

In this example, the area() function is marked virtual, which allows a base class pointer to invoke several area() implementations depending on the derived object it links to.

#include <iostream>
// Base class: Shape
class Shape {
protected:
    int width, height; // Data members common to all shapes 
public:
    // Constructor to initialize width and height
    Shape(int a = 0, int b = 0) : width(a), height(b) {}
    // Virtual function: area()
    // Declaring it virtual allows derived classes to provide their own implementations
    // and for the correct version to be called at runtime via a base class pointer 
    virtual int area() {
        std::cout << "Parent class area (Shape):" << std::endl;
        return 0; // Default implementation
    }
    // Virtual destructor is important for polymorphic base classes
    virtual ~Shape() = default;
};
// Derived class: Rectangle
class Rectangle : public Shape {
public:
    // Constructor calls base class constructor
    Rectangle(int a = 0, int b = 0) : Shape(a, b) {}
    // Override the virtual area() function for Rectangle
    // The 'override' keyword is good practice to ensure it's actually overriding
    int area() override {
        std::cout << "Rectangle class area:" << std::endl;
        return (width * height);
    }
};
// Derived class: Triangle
class Triangle : public Shape {
public:
    // Constructor calls base class constructor
    Triangle(int a = 0, int b = 0) : Shape(a, b) {}
    // Override the virtual area() function for Triangle
    int area() override {
        std::cout << "Triangle class area:" << std::endl;
        return (width * height / 2);
    }
};
int main() {
    Shape *shapePtr; // A base class pointer
    Rectangle rec(10, 7); // Create a Rectangle object
    Triangle tri(10, 5);  // Create a Triangle object
    // Assign address of Rectangle object to base class pointer
    shapePtr = &rec;
    // Call area() using the base class pointer.
    // The actual function called (Rectangle::area()) is determined at RUN TIME
    // because area() is virtual.
    std::cout << "Area of Rectangle: " << shapePtr->area() << std::endl;
    // Assign address of Triangle object to base class pointer
    shapePtr = &tri;
    // Call area() using the base class pointer.
    // The actual function called (Triangle::area()) is determined at RUN TIME.
    std::cout << "Area of Triangle: " << shapePtr->area() << std::endl;
    return 0;
}

Output

Area of Rectangle: Rectangle class area:
70
Area of Triangle: Triangle class area:
25

Explanation: ShapePtr is an instance of a pointer of type Shape*, which is its static type. Nonetheless, it can point to Triangle or Rectangle objects (according to their dynamic types). The corresponding area() method from the Rectangle or Triangle class is invoked when shapePtr->area() is called. The C++ runtime ascertains the true type of the object shapePtr points to. The core of dynamic binding is this.

Bookstore Pricing Strategy Example (Quote/Bulk_quote)

This example shows how to apply type-specific behaviour at runtime by using a base class reference to process objects of various derived types in a common function.

#include <iostream>
#include <string>
#include <vector> // Not directly used in this snippet but often for collections of polymorphic objects
// Base class: Quote for a book
class Quote {
public:
    Quote() = default; // Default constructor
    Quote(const std::string &book, double sales_price)
        : bookNo(book), price(sales_price) {}
    std::string isbn() const { return bookNo; }
    // Virtual function: net_price()
    // Derived classes can override this to apply different discount algorithms 
    virtual double net_price(std::size_t n) const {
        return n * price; // Default calculation: no discount
    }
    // Virtual destructor is crucial for proper dynamic memory management with polymorphic types
    virtual ~Quote() = default;
private:
    std::string bookNo; // ISBN number of this item
protected:
    double price = 0.0; // Normal, undiscounted price
};
// Derived class: Bulk_quote for bulk purchase discounts
class Bulk_quote : public Quote {
public:
    // Constructor calls base class constructor and initializes derived members
    Bulk_quote(const std::string &book, double sales_price,
               std::size_t qty, double disc)
        : Quote(book, sales_price), min_qty(qty), discount(disc) {}
    // Overrides the virtual net_price() function to apply a bulk discount
    double net_price(std::size_t n) const override { // 'override' keyword used for clarity 
        if (n >= min_qty)
            return n * (1 - discount) * price; // Apply discount
        else
            return n * price; // No discount if quantity is too low
    }
private:
    std::size_t min_qty = 0; // Minimum quantity for discount
    double discount = 0.0;   // Discount rate
};
// Function that processes items polymorphically
// Takes a const reference to the base class Quote 
double print_total(std::ostream &os, const Quote &item, size_t n) {
    // Calls either Quote::net_price or Bulk_quote::net_price based on item's dynamic type.
    // The decision on which 'net_price' to call is delayed until run time (dynamic binding).
    double ret = item.net_price(n);
    os << "ISBN: " << item.isbn() // Calls Quote::isbn (non-virtual, but accessible)
       << " # sold: " << n << " total due: " << ret << std::endl;
    return ret;
}
int main() {
    Quote basic("0-201-82470-1", 50.00);            // A basic Quote object
    Bulk_quote bulk("0-201-82470-1", 50.00, 10, 0.20); // A Bulk_quote object with discount
    // 'print_total' will call the appropriate 'net_price' version
    // depending on the dynamic type of the 'item' object passed.
    print_total(std::cout, basic, 20); // Passes a Quote object: calls Quote::net_price (no discount)
    print_total(std::cout, bulk, 20);  // Passes a Bulk_quote object: calls Bulk_quote::net_price (20% discount) 
    return 0;
}

Output

ISBN: 0-201-82470-1 # sold: 20 total due: 1000
ISBN: 0-201-82470-1 # sold: 20 total due: 800

Explanation: A const Quote& is the item parameter passed to the print_total function. The actual (dynamic) type of the object referenced by item determines which version of net_price should be executed when item.net_price(n) is called by the C++ runtime. Print_total now offers type-specific pricing behaviour for any Quote object without requiring compile-time knowledge of the derived type.

Dynamic polymorphism lets code operate with diverse objects using a single interface, making it valuable for adaptive and expandable software systems.

You can also read What Is Mean By Access Control In Inheritance C++ With Code?

Agarapu Geetha
Agarapu Geetha
My name is Agarapu Geetha, a B.Com graduate with a strong passion for technology and innovation. I work as a content writer at Govindhtech, where I dedicate myself to exploring and publishing the latest updates in the world of tech.
Index