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
virtualkeyword: Tells compiler to use dynamic binding - look up the actual object type at runtime -
Pure virtual functions:
= 0makes 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
overridekeyword 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:
Quick Quiz
- What is polymorphism?
- What's required for runtime polymorphism?
- What happens with object slicing?
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.
Output:
Error:
Lesson Progress
- Fix This Code
- Quick Quiz
- Practice Playground - run once