Intermediate 14 min

Polymorphism

Master polymorphism to write flexible code that works with objects of different types through a common interface

Master polymorphism to enable flexible, extensible code that works with objects of different types through a common interface.

A Simple Example

#include <iostream>
#include <string>
#include <vector>
#include <memory>

class Instrument {
protected:
    std::string name;
    std::string type;

public:
    Instrument(std::string name, std::string type)
        : name{name}, type{type} {
    }

    virtual void play() const = 0;  // Pure virtual

    virtual void tune() const {
        std::cout << \"Tuning \" << name << "\n";
    }

    virtual std::string getSound() const = 0;

    void displayInfo() const {
        std::cout << type << \": \" << name << "\n";
    }

    virtual ~Instrument() = default;
};

class Guitar : public Instrument {
private:
    int strings;

public:
    Guitar(std::string name, int strings = 6)
        : Instrument{name, \"String Instrument\"}, strings{strings} {
    }

    void play() const override {
        std::cout << \"Strumming \" << strings << \"-string guitar: \" << name << "\n";
        std::cout << \"Sound: \" << getSound() << "\n";
    }

    std::string getSound() const override {
        return \"Twang twang!\";
    }

    void tune() const override {
        std::cout << \"Tuning \" << strings << \" strings on \" << name << "\n";
    }
};

class Piano : public Instrument {
private:
    int keys;

public:
    Piano(std::string name, int keys = 88)
        : Instrument{name, \"Keyboard Instrument\"}, keys{keys} {
    }

    void play() const override {
        std::cout << \"Playing \" << keys << \"-key piano: \" << name << "\n";
        std::cout << \"Sound: \" << getSound() << "\n";
    }

    std::string getSound() const override {
        return \"Ding ding ding!\";
    }
};

class Drums : public Instrument {
private:
    int pieces;

public:
    Drums(std::string name, int pieces = 5)
        : Instrument{name, \"Percussion Instrument\"}, pieces{pieces} {
    }

    void play() const override {
        std::cout << \"Banging \" << pieces << \"-piece drum set: \" << name << "\n";
        std::cout << \"Sound: \" << getSound() << "\n";
    }

    std::string getSound() const override {
        return \"Boom boom crash!\";
    }

    void tune() const override {
        std::cout << \"Tightening drum heads on \" << name << "\n";
    }
};

// Polymorphic function - works with any Instrument
void performSong(const std::vector<Instrument*>& orchestra) {
    std::cout << \"\n=== Concert Beginning ===\" << "\n";

    std::cout << \"\nTuning instruments...\" << "\n";
    for (const Instrument* inst : orchestra) {
        inst->tune();  // Polymorphic call
    }

    std::cout << \"\nPerforming...\" << "\n";
    for (const Instrument* inst : orchestra) {
        inst->play();  // Polymorphic call
        std::cout << "\n";
    }
}

// Another polymorphic function
void showcaseInstrument(const Instrument& inst) {
    inst.displayInfo();
    std::cout << \"Let me demonstrate: \";
    inst.play();
}

int main() {
    std::vector<Instrument*> band;

    band.push_back(new Guitar{\"Fender Stratocaster\", 6});
    band.push_back(new Piano{\"Yamaha Grand\", 88});
    band.push_back(new Drums{\"Pearl Export\", 5});
    band.push_back(new Guitar{\"Bass Guitar\", 4});

    performSong(band);

    std::cout << \"\n=== Individual Showcases ===\" << "\n";
    for (const Instrument* inst : band) {
        showcaseInstrument(*inst);
        std::cout << "\n";
    }

    // Cleanup
    for (Instrument* inst : band) {
        delete inst;
    }

    return 0;
}

Breaking It Down

Virtual Functions

  • What they do: Enable runtime polymorphism by allowing derived classes to override base class methods
  • The virtual keyword: Tells compiler to use dynamic binding - look up the actual object type at runtime
  • Pure virtual functions: = 0 makes the base class abstract, forcing derived classes to implement
  • Remember: Virtual functions let you call the most specific version of a method based on object type

Base Class Pointers & References

  • What they do: Allow storing different derived types in the same collection or function parameter
  • Why needed: Polymorphism only works through pointers or references, not by value
  • Example: Instrument* can point to Guitar, Piano, or Drums objects
  • Remember: Pass by value causes object slicing - derived class data is lost

Override Keyword

  • What it does: Explicitly marks a method as overriding a virtual base class method
  • Safety: Compiler error if the method doesn't actually override anything
  • Best practice: Always use override keyword when overriding virtual functions
  • Remember: Catches typos and signature mismatches at compile time

Virtual Destructors

  • What they do: Ensure proper cleanup when deleting derived objects through base pointers
  • Why critical: Without virtual destructor, only base class destructor runs - memory leak
  • Rule of thumb: If a class has virtual functions, make destructor virtual
  • Remember: virtual ~ClassName() = default; is the modern way

Why This Matters

  • Polymorphism is the crown jewel of object-oriented programming.
  • It's what lets you write a graphics library that can draw any shape without knowing what shapes exist, or a plugin system where new plugins work without modifying existing code.
  • It's the principle behind "write once, extend forever." Every major framework, from Qt to Unity to web servers, leverages polymorphism.
  • Understanding it transforms how you think about code - from rigid, specific implementations to flexible, abstract designs.

Critical Insight

Polymorphism inverts control - instead of asking "what type are you?" and branching, you just call the method and let the object decide how to respond. This is the Open/Closed Principle - open for extension (add new types), closed for modification (existing code doesn't change):

// BAD: Type checking
void process(Shape* shape) {
    if (Circle* c = dynamic_cast<Circle*>(shape)) {
        // Circle-specific code
    } else if (Rectangle* r = dynamic_cast<Rectangle*>(shape)) {
        // Rectangle-specific code
    }
    // Need to modify this function for every new shape!
}

// GOOD: Polymorphism
void process(Shape* shape) {
    shape->draw();  // Just works for any shape!
    // Add new shapes without changing this code!
}

Best Practices

Always use virtual destructors: If a class has any virtual functions, make the destructor virtual with virtual ~ClassName() = default;

Use override keyword: Mark overriding methods with override to catch signature mismatches at compile time

Prefer smart pointers: Use std::unique_ptr or std::shared_ptr instead of raw pointers for automatic memory management

Pass by pointer/reference: Never pass polymorphic objects by value - use pointers or references to avoid object slicing

Common Mistakes

Slicing Objects: Passing objects by value instead of pointer/reference loses derived class information.

Forgetting Virtual Functions: Without virtual, you get compile-time binding, not runtime polymorphism.

Not Using Polymorphic Collections: Storing objects directly in vectors instead of pointers.

Missing virtual destructor: Causes memory leaks when deleting derived objects through base pointers.

Debug Challenge

This code has a bug that will cause a memory leak. Click the highlighted line to fix it:

1 class Animal {
2 public:
3 virtual void speak() const = 0;
4 ~Animal() = default;
5 };
6
7 class Dog : public Animal {
8 public:
9 void speak() const override {
10 std::cout << "Woof!" << "\n";
11 }
12 };
13
14 int main() {
15 Animal* pet{new Dog{}};
16 pet->speak();
17 delete pet; // Bug: Dog destructor won't run!
18 return 0;
19 }

Quick Quiz

  1. What is polymorphism?
The ability of objects to take multiple forms
Having many constructors
Using multiple inheritance
  1. What's required for runtime polymorphism?
Virtual functions and base class pointers/references
Templates
Function overloading
  1. What happens with object slicing?
Derived class data is lost when passing by value
The object is divided
Nothing bad

Practice Playground

Time to try out what you just learned! Play with the example code below, experiment by making changes and running the code to deepen your understanding.

Lesson Progress

  • Fix This Code
  • Quick Quiz
  • Practice Playground - run once