CSC166 Object Oriented Programming

Object Oriented ProgrammingUnit 613 min read

Polymorphism & Virtual Functions: Runtime Binding, Abstract Classes & Dynamic Dispatch

Unit 6 of Object Oriented Programming explores how polymorphism enables one interface to work with multiple data types at runtime, with a focus on virtual functions, abstract classes, and the mechanics of dynamic method binding. You’ll learn how to design flexible class hierarchies, implement pure virtual functions, an

TAKEAWAYS:

  • Runtime polymorphism uses virtual functions to bind method calls at execution time, enabling derived classes to override base class behavior (e.g., Animal::sound() → "hiss" for Snake).
  • Abstract classes (with pure virtual functions) define interfaces without implementations, forcing derived classes to provide concrete methods (e.g., Shape::area() must be implemented by Circle or Rectangle).
  • Dynamic dispatch relies on the vtable (virtual table) to resolve function calls at runtime, storing addresses of overridden methods per object.
  • Compile-time polymorphism (function overloading) differs from runtime polymorphism by resolving method calls during compilation based on argument types.
  • Virtual destructors are critical for safe deletion of derived objects via base pointers to prevent memory leaks.
  • Pure virtual functions (= 0) make a class abstract, while virtual functions (virtual) enable polymorphism in concrete classes.

Core Concepts: What Is Polymorphism?

Polymorphism (Greek for "many forms") allows objects of different classes to be treated uniformly through a common interface. There are two types:

1. Compile-Time Polymorphism (Static Binding)

  • Achieved via function overloading or operator overloading.
  • The function to call is determined at compile time based on argument types/syntax.
  • Example: Overloading + for int and string:
    class Math {
    public:
        int add(int a, int b) { return a + b; }
        string add(string a, string b) { return a + b; }
    };
    
    Visual: Overloading resolves at compile time like a router directing calls to the correct function based on input types.
    
    

2. Runtime Polymorphism (Dynamic Binding)

  • Achieved via virtual functions and inheritance.
  • The function to call is determined at runtime based on the actual object type.
  • Requires:
    • A base class with a virtual function.
    • A derived class overriding the function.
    • A base class pointer/reference pointing to a derived object.

Virtual Functions: How Runtime Polymorphism Works

Animal::sound0Dog::sound1Cat::sound2Animal vtable entryDog vtable entry (overrides)Cat vtable entry (overrides)
Virtual table (vtable) for Animal class with Dog and Cat overrides
inheritsinheritsAnimalDogCat
Inheritance hierarchy for Animal, Dog, and Cat classes with virtual function sound()

Mechanism Behind Virtual Functions

When a function is declared virtual in a base class, the compiler:

  1. Creates a virtual table (vtable) for the class, storing addresses of all virtual functions.
  2. Each object gets a vptr (virtual pointer) to its class’s vtable.
  3. At runtime, the vptr directs the call to the correct overridden function.

Example: Animal Sounds

class Animal {
public:
    virtual void sound() { cout << "Some sound"; } // Virtual function
};
class Dog : public Animal {
public:
    void sound() override { cout << "Raf-raf"; } // Overrides base
};
class Cat : public Animal {
public:
    void sound() override { cout << "Meow"; }
};

Trace:

Step Code Executed Output vptr Points To
Animal* a = new Dog(); Constructor calls (None) Dog::vtable
a->sound(); Dog::sound() via vptr "Raf-raf" Dog::sound
a = new Cat(); Reassigns vptr to Cat (None) Cat::vtable
a->sound(); Cat::sound() via vptr "Meow" Cat::sound

Visual: The vtable and vptr in action for Dog and Cat:

classDiagram
    class Animal {
        +virtual void sound()
    }
    class Dog {
        +void sound() "Raf-raf"
    }
    class Cat {
        +void sound() "Meow"
    }
    Animal <|-- Dog
    Animal <|-- Cat
    note for Animal "vtable:\n1. sound() -> Animal::sound\n2. vptr -> points to Dog/Cat vtable"
    note for Dog "vtable:\n1. sound() -> Dog::sound"
    note for Cat "vtable:\n1. sound() -> Cat::sound"

Abstract Classes and Pure Virtual Functions

An abstract class cannot be instantiated and is used to define a contract for derived classes.

  • Declared with at least one pure virtual function (= 0).
  • Forces derived classes to implement the missing methods.

Example: Shape Hierarchy

class Shape {
public:
    virtual double area() const = 0; // Pure virtual → abstract
    virtual ~Shape() {} // Virtual destructor for safe deletion
};
class Circle : public Shape {
private:
    double radius;
public:
    Circle(double r) : radius(r) {}
    double area() const override { return 3.14159 * radius * radius; }
};

Real-World Tie-In: eSewa’s Payment Processing eSewa uses polymorphism to handle different payment methods (credit card, mobile wallet, bank transfer) under a unified Payment abstract class. Each method (e.g., CreditCardPayment, KhaltiPayment) overrides processPayment(), ensuring consistent billing while adapting to each provider’s rules.


Virtual Destructors: Why They Matter

When deleting a derived object via a base pointer, the derived class destructor must run first to free its resources.

  • Solution: Declare the base class destructor virtual.
  • Example:
    class Base {
    public:
        virtual ~Base() { cout << "Base destructor\n"; } // Virtual!
    };
    class Derived : public Base {
    public:
        ~Derived() { cout << "Derived destructor\n"; }
    };
    
    Trace:
    Base* obj = new Derived();
    delete obj; // Output: "Derived destructor\nBase destructor\n"
    
    Visual: Destructor call chain:
    flowchart TD
        A["delete obj"] --> B["Base::~Base()\n(virtual lookup)"]
        B --> C["Derived::~Derived()\n(runs first)"]
        C --> B
classDiagram
    class Base {
        +virtual ~Base()
    }
    class Derived {
        +~Derived()
    }
    Base <|-- Derived
    note for Base "vtable:
    1. ~Base() → Base::~Base()"
    note for Derived "vtable:
    1. ~Base() → Derived::~Derived()"
    note for Derived "Destructor chain:
    Derived::~Derived() → Base::~Base()"
Destructor call chain with virtual destructor

Compile-Time vs. Runtime Polymorphism: Key Differences

Feature Compile-Time Polymorphism Runtime Polymorphism
Mechanism Function overloading/operator overloading Virtual functions + inheritance
Binding Time Compile time Runtime (dynamic binding)
Performance Faster (no vtable lookup) Slightly slower (vtable overhead)
Syntax Same function name, different args Same function name, virtual keyword
Example void print(int) vs void print(double) virtual void display() in base class
Use Case Math operations, I/O streams GUI event handling, plugin systems

Worked Example: Book Inventory System (Exam-Style)

Problem: A bookshop tracks books with title, price, and stock. Implement a system where:

  1. Books can be compared by price.
  2. Use polymorphism to display book details differently for Fiction and NonFiction.
NovelShort StoryFictionBiographyHistoryNonFictionBook
Book hierarchy with polymorphic display() methods

Solution:

class Book {
protected:
    string title;
    double price;
public:
    Book(string t, double p) : title(t), price(p) {}
    virtual void display() const { cout << "Title: " << title << ", Price: " << price << endl; }
    virtual ~Book() {}
    bool operator<(const Book& other) const { return price < other.price; }
};
class Fiction : public Book {
public:
    Fiction(string t, double p) : Book(t, p) {}
    void display() const override { cout << "Fiction: " << title << " (Price: " << price << ")" << endl; }
};
class NonFiction : public Book {
public:
    NonFiction(string t, double p) : Book(t, p) {}
    void display() const override { cout << "Non-Fiction: " << title << " (Price: " << price << ")" << endl; }
};

Trace: Comparing two books by price:

Book* b1 = new Fiction("Harry Potter", 500);
Book* b2 = new NonFiction("C++ Primer", 800);
bool cheaper = (*b1 < *b2); // true (500 < 800)

Real-World Tie-In: Daraz’s Order Queue Daraz uses polymorphism to prioritize orders. A base Order class with a virtual process() method is overridden by ExpressOrder (high priority) and StandardOrder (normal priority). The system dynamically routes orders to the correct processing queue based on the order type.


Common Pitfalls and Best Practices

  1. Forgetting virtual: Without virtual, the base class function is called, not the overridden one.
    // Wrong: No polymorphism!
    class Base { void show() { cout << "Base"; } };
    class Derived : public Base { void show() { cout << "Derived"; } };
    Base* b = new Derived();
    b->show(); // Output: "Base" (not "Derived")
    
  2. Not using override: Helps catch errors if the base function signature changes.
  3. Memory leaks: Always use virtual destructors when dealing with inheritance.
  4. Overusing abstract classes: Abstract classes should define interfaces, not implement shared logic (use protected members for that).

In the Real World

  1. eSewa’s Payment Gateway

    • Idea Used: Runtime polymorphism via an abstract PaymentMethod class.
    • How: Each payment type (CreditCard, Khalti, BankTransfer) inherits from PaymentMethod and overrides processPayment(). eSewa dynamically calls the correct method based on user selection.
    • Code Snippet:
      class PaymentMethod {
      public:
          virtual bool processPayment(double amount) = 0;
          virtual ~PaymentMethod() {}
      };
      class KhaltiPayment : public PaymentMethod {
      public:
          bool processPayment(double amount) override {
              // Khalti-specific logic
              return true;
          }
      };
      
  2. Ncell’s Billing System

    • Idea Used: Polymorphism for different tariff plans.
    • How: A base TariffPlan class with virtual calculateBill() is overridden by PrepaidPlan and PostpaidPlan. Ncell’s system routes calls to the correct billing logic at runtime.
    • Example Trace:
      TariffPlan* plan = new PostpaidPlan(1000); // 1000 minutes
      double bill = plan->calculateBill(); // Calls PostpaidPlan::calculateBill()
      
  3. Pathao’s Ride Allocation

    • Idea Used: Abstract Vehicle class with pure virtual calculateFare().
    • How: Derived classes (Bike, Car, Auto) implement fare calculations differently. Pathao’s dispatcher uses a Vehicle* to dynamically invoke the correct fare logic for each ride.

Exam Tip: How to Score Full Marks

  1. Define Key Terms Precisely:

    • Polymorphism: "The ability of an interface to be used for a general class of actions."
    • Virtual Function: "A member function declared with the virtual keyword, enabling dynamic binding."
    • Abstract Class: "A class with at least one pure virtual function that cannot be instantiated."
  2. Diagrams Are Mandatory:

    • Draw vtable diagrams for runtime polymorphism questions.
    • Show inheritance hierarchies with overridden methods highlighted.
    • Example for a question on Animal sounds:
      
      
  3. Code + Trace = Full Marks:

    • Always provide a complete class hierarchy with virtual/override.
    • Include a trace table for runtime behavior (e.g., vptr changes).
    • Example for a question on function overriding:
      // Question: Explain how this outputs "Derived".
      class Base { virtual void show() { cout << "Base"; } };
      class Derived : public Base { void show() override { cout << "Derived"; } };
      int main() { Base* b = new Derived(); b->show(); }
      
      Answer Trace:
      Step Action Output
      Base* b = ... vptr points to Derived::vtable (None)
      b->show() Calls Derived::show() via vptr "Derived"
  4. Real-World Applications:

    • Link abstract classes to system design (e.g., "A PaymentProcessor abstract class ensures all payment methods implement validate() and charge()").
    • Use Nepali examples (e.g., "NTC’s ServiceProvider hierarchy uses polymorphism to route calls to LandlineService or MobileService").
  5. Avoid Common Mistakes:

    • ❌ Forgetting virtual in base class → 0 marks for runtime polymorphism.
    • ❌ Not using override → partial marks (shows lack of understanding).
    • ❌ Memory leaks in destructor questions → deduct marks for unsafe code.

Practice Questions (Exam-Style)

  1. Short Answer:

    • Differentiate between virtual and pure virtual functions with examples.
    • Why is a virtual destructor necessary in a base class with derived classes?
  2. Programming:

    • Implement a Vehicle hierarchy with Car and Bike classes. Override displaySpecs() to show different details for each. Use a Vehicle* to demonstrate polymorphism.
  3. Trace the Output:

    class Base { virtual void show() { cout << "Base"; } };
    class Derived : public Base { void show() { cout << "Derived"; } };
    int main() {
        Base* b = new Derived();
        b->show();
        delete b;
    }
    

    Expected Output: Derived (with destructors called in reverse order).

  4. Design:

    • Create an abstract Employee class with pure virtual calculateSalary(). Derive FullTimeEmployee and PartTimeEmployee to implement it. Explain how this helps HR systems.

Based on the TU BSc CSIT syllabus for Object Oriented Programming (CSC166), unit 6.

Discussion

Loading…