Polymorphism in OOP: Overloading, Overriding, vtable

Polymorphism in OOP: compile-time overloading vs run-time overriding, dynamic dispatch and the vtable, upcasting, downcasting and object slicing.

What is polymorphism in OOP?

Polymorphism means one interface with many implementations: the same call does different things depending on the types involved. Compile-time polymorphism is method and operator overloading, resolved by the compiler from argument types. Run-time polymorphism is method overriding, where the object's actual class decides which method runs through dynamic dispatch — virtual functions and a vtable in C++.

Polymorphism — Greek for "many forms" — is the ability of one interface to work with many types. A single call, employee.pay(), does the right thing for a manager, an engineer and an intern; a single operator, +, adds numbers, joins strings and adds vectors. It is the pillar that lets you add a new class without editing the code that uses it. This note separates the two kinds interviewers ask about — compile-time and run-time — explains how a virtual call actually works (the vtable), and covers upcasting, downcasting and C++'s object slicing.

What polymorphism means

There are two moments at which a program can decide which code a call runs:

  • Compile time (static or early binding) — the compiler picks the function from the declared types of the arguments. This is overloading: several functions share a name but have different parameter lists, and operator overloading.
  • Run time (dynamic or late binding) — the program picks the method while it runs, from the actual class of the object. This is overriding with dynamic dispatch.
compile time: the compiler decidesrun time: the object decidese->pay()declared type of e: Employee*call through vtable slot paywhich function: not known yetManager::pay → 150000e = staff[0], a ManagerEngineer::pay → 90000e = staff[1], a EngineerIntern::pay → 25000e = staff[2], a Intern
Overloads are chosen by the compiler; overrides by the object. Example: area(5); area(3, 4); for e in staff: e->pay()
  1. area(5) has 1 int argument, so only int area(int side) fits and the other overload is rejected for its parameter count. The choice is fixed in the compiled code; it returns 25.
  2. area(3, 4) has 2 int arguments, so only int area(int width, int height) fits and the other overload is rejected for its parameter count. The choice is fixed in the compiled code; it returns 12.
  3. For e->pay() the compiler sees only Employee*, so it can only emit "call whatever is in slot pay". The one call site then runs Manager::pay, Engineer::pay, Intern::pay in turn, chosen by each object.

Type theory names a third kind, parametric polymorphism: one generic piece of code that works for any type, such as List<T> in Java or vector<T> in C++. C++ templates are compiled into a separate version per type; Java generics are compiled once and the type parameters are erased.

Compile-time polymorphism

Method overloading

Two methods overload each other when they share a name and differ in the number, types or order of their parameters. The compiler picks the best match for each call.

  • A difference in return type alone is not enough: int area(int) and long area(int) cannot coexist, because the call area(5) gives the compiler nothing to choose by.
  • Java resolves overloads in phases: first without boxing or varargs (but with widening such as int to long), then with boxing, then with varargs. So with f(long) and f(Integer) both present, f(5) calls f(long).
  • C++ reports an error when two candidates match equally well — an ambiguous call.
  • Python and JavaScript have no overloading. A second def area(...) replaces the first. Use default arguments, *args, or functools.singledispatch, which dispatches on the first argument's type at run time.

Operator overloading

Operator overloading gives an operator a meaning for your own class. C++ defines operator+, operator== and so on as functions; Python maps operators to special methods (+ calls __add__, == calls __eq__, len() calls __len__). Java and JavaScript do not allow it, so their classes use named methods such as plus.

#include <iostream>
using namespace std;

int area(int side) { return side * side; }         // two overloads: the compiler
int area(int width, int height) { return width * height; }  // picks one per call

struct Vec2 {
    int x, y;
    Vec2 operator+(const Vec2& o) const { return {x + o.x, y + o.y}; }  // operator overloading
};

ostream& operator<<(ostream& out, const Vec2& v) {  // lets cout print a Vec2
    return out << "(" << v.x << ", " << v.y << ")";
}

int main() {
    cout << "square area: " << area(5) << "\n";
    cout << "rectangle area: " << area(3, 4) << "\n";
    Vec2 a{1, 2}, b{3, 4};
    cout << a << " + " << b << " = " << a + b << "\n";
    return 0;
}
class Vec2 {
    final int x, y;
    Vec2(int x, int y) { this.x = x; this.y = y; }
    Vec2 plus(Vec2 o) { return new Vec2(x + o.x, y + o.y); }  // no operator overloading in Java
    @Override public String toString() { return "(" + x + ", " + y + ")"; }
}

public class Main {
    static int area(int side) { return side * side; }               // two overloads: the compiler
    static int area(int width, int height) { return width * height; } // picks one per call

    public static void main(String[] args) {
        System.out.println("square area: " + area(5));
        System.out.println("rectangle area: " + area(3, 4));
        Vec2 a = new Vec2(1, 2), b = new Vec2(3, 4);
        System.out.println(a + " + " + b + " = " + a.plus(b));
    }
}
def area(width, height=None):  # one function; a default argument stands in for overloading
    return width * width if height is None else width * height


class Vec2:
    def __init__(self, x, y):
        self.x, self.y = x, y

    def __add__(self, other):  # a + b calls a.__add__(b)
        return Vec2(self.x + other.x, self.y + other.y)

    def __str__(self):
        return f"({self.x}, {self.y})"


print(f"square area: {area(5)}")
print(f"rectangle area: {area(3, 4)}")
a, b = Vec2(1, 2), Vec2(3, 4)
print(f"{a} + {b} = {a + b}")
function area(width, height) { // no overloading: one function checks its arguments
  return height === undefined ? width * width : width * height;
}

class Vec2 {
  constructor(x, y) { this.x = x; this.y = y; }
  plus(o) { return new Vec2(this.x + o.x, this.y + o.y); } // no operator overloading in JS
  toString() { return `(${this.x}, ${this.y})`; }
}

console.log(`square area: ${area(5)}`);
console.log(`rectangle area: ${area(3, 4)}`);
const a = new Vec2(1, 2), b = new Vec2(3, 4);
console.log(`${a} + ${b} = ${a.plus(b)}`);
square area: 25
rectangle area: 12
(1, 2) + (3, 4) = (4, 6)

Run-time polymorphism

Overriding and dynamic dispatch

A subclass overrides an inherited method by defining one with the same signature. When code calls that method through a reference of the parent type, the program looks at the object's real class and runs that class's version. This is dynamic dispatch, and it is what lets payroll(employees) handle a class that did not exist when payroll was written.

Which methods dispatch dynamically differs by language:

  • C++ — only functions declared virtual in the base class. A non-virtual function is bound at compile time to the declared type.
  • Java — every instance method that is not static, private or final. (A final method cannot be overridden, so there is nothing to choose between.)
  • Python and JavaScript — every method; the name is looked up on the object's class each time it is called.

The program below keeps a list of Employee references holding three different classes. The loop never asks what each one is — each object's own pay() runs. At the end it downcasts to find the managers, the only ones with an approveLeave method.

#include <iostream>
#include <memory>
#include <string>
#include <vector>
using namespace std;

class Employee {
protected:
    string name;
public:
    Employee(string n) : name(n) {}
    virtual ~Employee() = default;
    virtual string role() const = 0;
    virtual int pay() const = 0;  // dispatched at run time
    void describe() const { cout << name << " (" << role() << "): " << pay() << "\n"; }
};

class Manager : public Employee {
    int salary, bonus;
public:
    Manager(string n, int s, int b) : Employee(n), salary(s), bonus(b) {}
    string role() const override { return "Manager"; }
    int pay() const override { return salary + bonus; }
    void approveLeave() const { cout << name << " approves leave\n"; }
};

class Engineer : public Employee {
    int salary;
public:
    Engineer(string n, int s) : Employee(n), salary(s) {}
    string role() const override { return "Engineer"; }
    int pay() const override { return salary; }
};

class Intern : public Employee {
    int stipend;
public:
    Intern(string n, int s) : Employee(n), stipend(s) {}
    string role() const override { return "Intern"; }
    int pay() const override { return stipend; }
};

int main() {
    vector<unique_ptr<Employee>> staff;
    staff.push_back(make_unique<Manager>("Meera", 120000, 30000));
    staff.push_back(make_unique<Engineer>("Arjun", 90000));
    staff.push_back(make_unique<Intern>("Riya", 25000));

    int total = 0;
    for (const auto& e : staff) {  // upcast: every element is used as an Employee
        e->describe();
        total += e->pay();
    }
    cout << "Payroll total: " << total << "\n";

    for (const auto& e : staff)  // checked downcast: nullptr unless e really is a Manager
        if (auto m = dynamic_cast<const Manager*>(e.get())) m->approveLeave();
    return 0;
}
import java.util.List;

abstract class Employee {
    protected final String name;
    Employee(String name) { this.name = name; }
    abstract String role();
    abstract int pay();  // dispatched at run time
    void describe() { System.out.println(name + " (" + role() + "): " + pay()); }
}

class Manager extends Employee {
    private final int salary, bonus;
    Manager(String n, int s, int b) { super(n); salary = s; bonus = b; }
    String role() { return "Manager"; }
    int pay() { return salary + bonus; }
    void approveLeave() { System.out.println(name + " approves leave"); }
}

class Engineer extends Employee {
    private final int salary;
    Engineer(String n, int s) { super(n); salary = s; }
    String role() { return "Engineer"; }
    int pay() { return salary; }
}

class Intern extends Employee {
    private final int stipend;
    Intern(String n, int s) { super(n); stipend = s; }
    String role() { return "Intern"; }
    int pay() { return stipend; }
}

public class Main {
    public static void main(String[] args) {
        List<Employee> staff = List.of(  // upcast: every element is used as an Employee
            new Manager("Meera", 120000, 30000), new Engineer("Arjun", 90000), new Intern("Riya", 25000));

        int total = 0;
        for (Employee e : staff) {
            e.describe();
            total += e.pay();
        }
        System.out.println("Payroll total: " + total);

        for (Employee e : staff)  // checked downcast with a type pattern (Java 16+)
            if (e instanceof Manager m) m.approveLeave();
    }
}
class Employee:
    def __init__(self, name):
        self.name = name

    def describe(self):
        print(f"{self.name} ({self.role()}): {self.pay()}")


class Manager(Employee):
    def __init__(self, name, salary, bonus):
        super().__init__(name)
        self.salary, self.bonus = salary, bonus

    def role(self): return "Manager"
    def pay(self): return self.salary + self.bonus
    def approve_leave(self): print(f"{self.name} approves leave")


class Engineer(Employee):
    def __init__(self, name, salary):
        super().__init__(name)
        self.salary = salary

    def role(self): return "Engineer"
    def pay(self): return self.salary


class Intern(Employee):
    def __init__(self, name, stipend):
        super().__init__(name)
        self.stipend = stipend

    def role(self): return "Intern"
    def pay(self): return self.stipend


staff = [Manager("Meera", 120000, 30000), Engineer("Arjun", 90000), Intern("Riya", 25000)]
total = 0
for e in staff:  # each object's own pay() runs
    e.describe()
    total += e.pay()
print(f"Payroll total: {total}")

for e in staff:
    if isinstance(e, Manager):  # Python needs no cast, only a type check
        e.approve_leave()
Meera (Manager): 150000
Arjun (Engineer): 90000
Riya (Intern): 25000
Payroll total: 265000
Meera approves leave

Notice describe() in the base class: it calls role() and pay(), and those calls dispatch to the subclass even though describe itself was written in Employee. A base class can therefore define the outline of an algorithm and leave steps to subclasses — the Template Method pattern.

How a virtual call works: the vtable

C++ compilers implement virtual functions with a virtual table (vtable): one array of function pointers per class, one slot per virtual function, which every object reaches through a hidden pointer, the vptr, set by its constructor. The standard does not require it, but GCC, Clang and MSVC all work this way:

objectsone vtable per classMeera : Managervptrname = Meerasalary = 120000bonus = 30000Manager's vtabledestructor → ~Managerrole → Manager::rolepay → Manager::payArjun : Engineervptrname = Arjunsalary = 90000Engineer's vtabledestructor → ~Engineerrole → Engineer::rolepay → Engineer::payRiya : Internvptrname = Riyastipend = 25000Intern's vtabledestructor → ~Internrole → Intern::rolepay → Intern::pay = 25000
How e->pay() finds the right function: vptr, vtable, slot. Example: staff = [Manager("Meera"), Engineer("Arjun"), Intern("Riya")]
  1. Every object of a class with virtual functions carries a hidden vptr, set by its constructor, pointing at its class's vtable. Each class's table has the same slots in the same order; an override fills its slot with that class's own function.
  2. e->pay() for staff[0] compiles to: follow e's vptr, read the third slot (pay), call what is there. Here that is Manager::pay, which returns 120000 + 30000 = 150000.
  3. The same machine code for staff[1] reads a different vptr and so a different table, and runs Engineer::pay: 90000.
  4. The same machine code for staff[2] reads a different vptr and so a different table, and runs Intern::pay: 25000. The loop never asked which class it held; the total is 265000.

The cost is one pointer per object, one table per class and an indirect call that the compiler usually cannot inline — negligible in most code, noticeable in very tight loops. Java's JVM uses the same idea (method tables), and its JIT compiler often removes the indirection when it can prove only one class is ever seen at a call site. Python looks the method name up on the object's class at each call, through the MRO.

Upcasting and downcasting

Upcasting is treating a child object as its parent type: putting a Manager into a list of Employee. It is always safe and implicit, because every manager is an employee. Downcasting goes the other way — treating an Employee reference as a Manager — and is safe only if the object really is a manager, so it must be checked.

staff: Employee*dynamic_cast<Manager*>staff[0]Meera : ManagerManager* (valid)staff[1]Arjun : Engineernullptrstaff[2]Riya : Internnullptr> Meera approves leave
Upcasting is implicit; a downcast is checked. Example: for e in staff: if (auto m = dynamic_cast<const Manager*>(e)) m->approveLeave();
  1. Upcasting: each element is used through an Employee pointer, whatever its real class. That is always safe, because every Manager, Engineer and Intern is an Employee.
  2. dynamic_cast checks the object's real class at run time: Meera is a Manager, so the cast returns a usable Manager pointer and approveLeave() can be called.
  3. Arjun is an Engineer, which is not a Manager anywhere up its chain (Engineer → Employee), so the cast returns nullptr instead of a pointer to the wrong thing.
  4. Riya is an Intern, which is not a Manager anywhere up its chain (Intern → Employee), so the cast returns nullptr instead of a pointer to the wrong thing.
C++JavaPython
UpcastImplicit: Employee* e = &m;ImplicitNot needed
Checked downcastdynamic_cast<Manager*>(e) (base must have a virtual function)instanceof, or e instanceof Manager misinstance(e, Manager)
Unchecked downcaststatic_cast<Manager*>(e)None: every cast is checked at run timeNot needed: just call the method
Wrong typenullptr for pointers, std::bad_cast for references; undefined behaviour with static_castClassCastExceptionAttributeError when the method is missing

A chain of downcasts (if it is a Manager... else if it is an Intern...) is usually a sign that the behaviour belongs in an overridden method instead.

Object slicing in C++

Polymorphism in C++ works only through pointers and references. If you copy a derived object into a base-class value, only the base part is copied — the derived part is sliced off — and the copy really is a base object, so its virtual calls run the base versions:

#include <iostream>
#include <string>
using namespace std;

class Employee {
public:
    virtual ~Employee() = default;
    virtual string role() const { return "Employee"; }
};

class Manager : public Employee {
public:
    string role() const override { return "Manager"; }
};

void byValue(Employee e) { cout << "by value: " << e.role() << "\n"; }  // copies only the Employee part
void byReference(const Employee& e) { cout << "by reference: " << e.role() << "\n"; }

int main() {
    Manager m;
    byValue(m);
    byReference(m);
    Employee copy = m;  // sliced as well
    cout << "assigned copy: " << copy.role() << "\n";
    return 0;
}
by value: Employee
by reference: Manager
assigned copy: Employee
m : ManagerEmployee part: vptrManager partManager's vtablerole → Manager::rolee : const Employee&a reference: another name for mbyReference(m): e.role() → "Manager"
Object slicing: a derived object copied into a base value. Example: Manager m; byValue(m); byReference(m);
  1. Passing m by value copies only the Employee part into a new Employee object, whose constructor points its vptr at Employee's vtable. The Manager part is cut away, so role() answers "Employee". Employee copy = m slices the same way.
  2. By reference nothing is copied: e is another name for m itself, the vptr still points at Manager's vtable, and role() answers "Manager". Polymorphism in C++ needs a pointer or a reference.

Java and Python cannot slice, because their variables only ever hold references.

Overloading vs overriding

AspectOverloadingOverriding
What it isSame name, different parametersSubclass redefines an inherited method
ParametersMust differMust be the same
Return typeMay differ, but cannot be the only differenceSame, or a subtype (covariant)
DecidedAt compile time, from argument typesAt run time, from the object's class
Kind of polymorphismCompile-time (static)Run-time (dynamic)
Needs inheritanceNoYes
Static, private, final methodsCan be overloadedCannot be overridden
In PythonNot supported; the last definition winsSupported

Common mistakes

  • Trying to overload on return type alone; the compiler rejects it.
  • Forgetting virtual in C++ and wondering why the base version runs through a base pointer.
  • Passing a C++ derived object to a function that takes the base by value, slicing it.
  • Writing two def methods with the same name in Python and expecting overloading.
  • Calling an overridable method from a Java constructor; it runs the subclass version before the subclass's fields are initialised.
  • Using static_cast for a downcast whose type you have not checked.

Interview questions

Can a method be overloaded on return type only? No, in C++ and Java. A call such as area(5) must be resolvable from its arguments, and the return type does not appear at the call site — it may even be discarded. Java reports the second method as "already defined"; C++ reports that the functions cannot be overloaded.

Can a constructor be virtual in C++? No. A virtual call needs the object's vptr, which the constructor itself sets up; and when you construct an object you already name its exact type. To create a copy of an object whose type you do not know, give the base class a virtual clone() method. Destructors, on the other hand, should be virtual in any base class used polymorphically.

What happens if a constructor calls a virtual method? In C++ the call dispatches to the class whose constructor is running, not the most derived class, because the derived part does not exist yet. In Java and Python the most derived override runs — before the subclass's own constructor has initialised its fields, so it sees default values such as null or 0. Both are reasons to avoid calling overridable methods from constructors.

What is the difference between early binding and late binding? Early (static) binding fixes which function a call runs at compile time — overloads, non-virtual C++ functions, and Java's static, private and final methods. Late (dynamic) binding chooses at run time from the object's actual class — virtual functions in C++, ordinary instance methods in Java, every method in Python.

What is the difference between static_cast and dynamic_cast? static_cast converts at compile time with no run-time check, so a wrong downcast is undefined behaviour. dynamic_cast checks the object's real type at run time using its type information, returning nullptr for a failed pointer cast or throwing std::bad_cast for a failed reference cast, and it requires the base class to have at least one virtual function.

How does Python achieve polymorphism without overloading or declared types? Through duck typing: any object with the right method can be used, whatever its class, so a function that calls obj.pay() works for anything with a pay method. Overloading-like behaviour comes from default arguments, *args, and functools.singledispatch.

Are generics and templates a form of polymorphism? Yes, parametric polymorphism: one definition works for many types. C++ templates generate a separate compiled version for each type used. Java generics are checked at compile time and then erased, so one compiled class serves every type argument and the type is not available at run time.

Can you achieve run-time polymorphism with data members? No. Fields are not overridden: in Java, a subclass field with the same name hides the parent's, and which one you read depends on the reference's declared type. Only methods dispatch dynamically, which is one more reason to keep fields private and expose behaviour through methods.

Next, read Abstraction: Abstract Classes and Interfaces, then check yourself with the OOP Intermediate skill test.

Common questions

What are the types of polymorphism in OOP?

Interviews usually expect two: compile-time (static) polymorphism, which is method and operator overloading, and run-time (dynamic) polymorphism, which is method overriding with dynamic dispatch. Type theory adds parametric polymorphism, which is generics in Java and templates in C++, where one piece of code works for many types.

What is the difference between method overloading and method overriding?

Overloading is several methods with the same name but different parameter lists, usually in one class, chosen by the compiler from the argument types. Overriding is a subclass redefining an inherited method with the same signature, chosen at run time from the object's actual class. Overloading needs no inheritance; overriding does.

Does Python support method overloading?

Not by signature. Defining a second method with the same name simply replaces the first. Python gets the same effect with default argument values, *args and **kwargs, or functools.singledispatch, which picks an implementation from the type of the first argument at run time.

What is a virtual function in C++?

A virtual function is a member function declared with the virtual keyword in a base class so that a call through a base pointer or reference runs the derived class's override. Without virtual, the call is bound at compile time to the version in the pointer's declared type.

Does Java support operator overloading?

No. Java does not let programmers define operators for their own classes. The one built-in exception is + on String, which the compiler turns into string concatenation. Classes like BigInteger and BigDecimal therefore offer methods such as add and multiply instead of operators.

Test yourself

← Inheritance · Abstraction: Abstract Classes and Interfaces →