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
FeatureCompile-timeRuntime
MechanismFunction / operator overloadingvirtual functions + vtable
When resolvedAt compile timeAt runtime
Keyword neededNonevirtual
Requires pointer/refNoYes (for dispatch to work)
PerformanceZero overheadOne pointer dereference per call
Typical useSame operation, different typesFamily 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: