C++: OOP, Polymorphism
Learn how one interface can take many forms: compile-time resolution through overloading, runtime dispatch through virtual functions, and the rules that keep it all safe.
Two Kinds of Polymorphism
Polymorphism means "many forms", the same function name or operator produces different behavior depending on context. C++ provides two varieties: compile-time polymorphism, where the compiler picks the right function before the program runs, and runtime polymorphism, where the decision is made while the program is executing based on the actual type of the object.
Compile-time vs Runtime Polymorphism
Compile-time is faster because the call is resolved by the compiler. Runtime is more flexible because the same base pointer can call the correct function on any derived object.
- Compile-time: function overloading, operator overloading, resolved by the compiler from argument types
- Runtime: virtual functions, resolved at execution time through the vtable
- Both use the same idea: one name, multiple behaviors
- Runtime polymorphism requires a pointer or reference to a base class, it does not work through value variables
Function Overloading
Function overloading lets you define multiple functions with the same name in the same scope, as long as their parameter lists differ. The compiler inspects the argument types at the call site and picks the matching version automatically.
Function Overloading
Same name, different signatures. The compiler resolves the call at compile time based on the number and types of the arguments provided.
- Functions must differ in parameter count or types, return type alone is not enough
- The compiler performs overload resolution and picks the best match
- Keeps the API clean: add(2, 3) and add(1.5, 2.5) are intuitive without inventing new names
- All overloads must be in the same scope; a derived class overload does not override, it hides
Calculator with Overloaded add()
C++Three functions named add. The compiler picks the right one from the argument types.
Operator Overloading
Operator overloading is a special form of compile-time polymorphism that gives built-in operators (+, -, <<, and others) custom meanings for user-defined types. The function name is operator followed by the symbol.
Operator Overloading
Overload an operator to make user-defined types feel as natural to use as built-in types. c1 + c2 reads far better than c1.add(c2).
- Syntax: Complex operator+(const Complex& other) const { ... }
- The left operand is the object the function is called on (this)
- Overload operator<< as a friend function to enable cout << obj syntax
- Cannot overload: :: (scope), . (member access), .* (member pointer), ?: (ternary)
Complex Number with +, -, and <<
C++Operators feel natural. operator<< is a friend so it can access real and imag directly.
Virtual Functions and Runtime Polymorphism
Adding the virtual keyword to a base class function tells the compiler to defer the call decision until runtime. When you call a virtual function through a base class pointer or reference, C++ checks the actual type of the object at that moment and dispatches to the correct derived version.
virtual function
One base pointer, many possible behaviors, determined at runtime by the object's actual type, not the pointer's type.
- Declare in base: virtual void draw() { ... }
- Override in derived: void draw() { ... }, the derived version replaces the base
- Call through a base pointer: ptr->draw() calls the right version at runtime
- Without virtual, the base version is always called regardless of the actual object type
Virtual draw() on a Shape Hierarchy
C++The same loop, the same pointer type, but each object calls its own draw() at runtime.
The vtable: How Runtime Dispatch Works
When a class has at least one virtual function, the compiler builds a vtable (virtual function table) for it: a hidden array of function pointers, one entry per virtual function. Every object of that class holds a hidden pointer (the vptr) to its class's vtable. When a virtual function is called through a pointer, C++ follows the vptr to the vtable and calls the right function.
vtable Mechanics
The vtable is built at compile time; the lookup happens at runtime. This one indirection is the entire cost of runtime polymorphism.
- Each class with virtual functions gets its own vtable
- Each object stores a hidden vptr pointing to its class's vtable
- A virtual call dereferences vptr, looks up the function pointer, and calls it
- The slight runtime overhead (one pointer dereference) is the price for flexible dispatch
Pure Virtual Functions and Abstract Classes
A pure virtual function is declared with = 0 at the end. It has no implementation in the base class. Any class that contains at least one pure virtual function becomes an abstract class, it cannot be instantiated directly and only serves as a blueprint that derived classes must complete.
Pure Virtual and Abstract Class
Use an abstract base class to define a contract. Every concrete derived class must implement all pure virtual functions before objects can be created from it.
- Syntax: virtual double area() const = 0;
- A class with any pure virtual function is abstract, you cannot write: Shape s;
- A derived class that does not override all pure virtuals is also abstract
- Abstract classes are ideal for defining shared interfaces across unrelated types
Abstract Shape with Pure Virtual area() and draw()
C++Shape cannot be instantiated. Circle and Rectangle must implement both pure virtuals to be usable.
Virtual Destructors
When you delete a derived object through a base class pointer, only the base destructor runs, unless the destructor is declared virtual. Without a virtual destructor, the derived class's destructor is skipped, leaving any resources it would have freed as a leak. Always declare the base destructor virtual when the class is designed to be inherited.
virtual ~BaseClass()
If a base class destructor is not virtual, deleting a derived object through a base pointer causes undefined behavior and resource leaks.
- Rule: if a class has any virtual function, also make its destructor virtual
- Without virtual destructor: delete basePtr calls only ~Base(), skips ~Derived()
- With virtual destructor: delete basePtr calls ~Derived() first, then ~Base()
- The fix is one word: add virtual in front of the base class destructor
Virtual Destructor
C++Because ~Base() is virtual, delete ptr calls ~Derived() first, freeing data, then ~Base().
override and final (C++11)
C++11 introduced two specifiers that make inheritance safer. override tells the compiler "this function is intended to override a base virtual", if no matching virtual exists, it is a compile error. final prevents any further overriding of a function, or prevents a class from being inherited at all.
override and final
Use override on every function that overrides a virtual. Use final when a class or function must not be extended further.
- override: catches typos, if you misspell the function name, the compiler errors instead of silently creating a new function
- override: catches signature mismatches, wrong parameter type produces a compile error
- final on a function: void draw() final, no derived class can override draw() further
- final on a class: class Concrete final, no class can inherit from Concrete
override and final in Action
C++override ensures draw() matches a real virtual. final locks it: nothing can override draw() past FinalCircle.
Compile-time vs Runtime Polymorphism: Summary
Both forms achieve the same goal, one name, many behaviors, through different mechanisms. Choosing between them depends on whether the type decision can be made at compile time or must wait until runtime.
Choosing the Right Form
Use compile-time polymorphism when the types are known at the call site. Use runtime polymorphism when you need to handle a family of related types through a common interface.
- Compile-time: faster, zero runtime overhead, resolved entirely by the compiler
- Runtime: flexible, works through base pointers and references, allows adding new types without changing existing code
- Operator overloading is always compile-time
- Virtual functions are always runtime, even if only one type exists at a given call site
| Feature | Compile-time | Runtime |
|---|---|---|
| Mechanism | Function / operator overloading | virtual functions + vtable |
| When resolved | At compile time | At runtime |
| Keyword needed | None | virtual |
| Requires pointer/ref | No | Yes (for dispatch to work) |
| Performance | Zero overhead | One pointer dereference per call |
| Typical use | Same operation, different types | Family of related types through a base interface |
Knowledge Check
1. What is compile-time polymorphism?
2. What keyword enables runtime polymorphism in C++?
3. What is a pure virtual function?
4. Can you create an object directly from an abstract class?
5. Why should base class destructors be declared virtual?
6. What does the override keyword do in C++11?
7. What does the final keyword do when applied to a class?
8. What is the vtable?
9. Function overloading differs from function overriding because: