2Brute2Best
🔥0
F2

Inheritance & Polymorphism

Runtime PolymorphismVirtual Dispatch override final
⚡ 60-Second Architecture SummaryF2 · Foundation
4 Core ConceptsC++23 Verified Demo4 Rules of Thumb
Why It Matters in High-Scale Systems

Runtime polymorphism is the mechanism that allows callers to manipulate heterogeneous objects uniformly without branching on type tags or enums. In C++, dynamic dispatch is opt-in via the virtual keyword. Understanding how virtual tables dispatch calls, how the compiler enforces overrides, and why missing a virtual destructor causes silent undefined behavior and memory leaks is fundamental to writing stable object-oriented architectures.

Core Mental Model

Imagine a standardized electrical socket interface (DeliveryPartner). Any appliance—whether a refrigerator (BikeDeliveryPartner) or an induction stove (DroneDeliveryPartner)—plugs into the socket. The wall wiring (dispatchFleetOrder) simply switches current on without caring what specific internal circuitry consumes it. The hidden vptr inside the device ensures the right motor turns on.

01

Why this matters in LLD interviews

Runtime polymorphism is the mechanism that allows callers to manipulate heterogeneous objects uniformly without branching on type tags or enums. In C++, dynamic dispatch is opt-in via the virtual keyword. Understanding how virtual tables dispatch calls, how the compiler enforces overrides, and why missing a virtual destructor causes silent undefined behavior and memory leaks is fundamental to writing stable object-oriented architectures.

Mental model

Imagine a standardized electrical socket interface (DeliveryPartner). Any appliance—whether a refrigerator (BikeDeliveryPartner) or an induction stove (DroneDeliveryPartner)—plugs into the socket. The wall wiring (dispatchFleetOrder) simply switches current on without caring what specific internal circuitry consumes it. The hidden vptr inside the device ensures the right motor turns on.

02

Core concepts

2.1Static vs Dynamic Dispatch & The virtual Keyword

By default, C++ binds member function calls at compile time (static dispatch) based on the declared pointer or reference type. If a function is not declared virtual, calling partnerPtr->dispatch(orderId) always executes the base version, even if the underlying object is a BikeDeliveryPartner.

Declaring a method virtual instructs the compiler to perform dynamic dispatch: it defers binding until runtime, looking up the actual runtime type's function implementation through the virtual table.

C++23 snippet
class DeliveryPartner {
public:
    virtual ~DeliveryPartner() = default;
    virtual std::string dispatch(std::string_view orderId) = 0;
};

2.2override and final: Eliminating Silent Bugs

Before C++11, a slight typo in a derived method signature (such as a missing const, a different parameter type, or an int vs int64_t) did not trigger any compiler error. Instead, the compiler silently treated it as a brand-new unrelated function, leaving the base virtual function un-overridden!

Always tag overriding methods with override. The compiler immediately fails if no matching virtual base function exists. Use final when a class or method should never be derived further; this lets optimizing compilers perform devirtualization (inlining the call and bypassing the vtable entirely).

C++23 snippet
class BikeDeliveryPartner final : public DeliveryPartner {
public:
    std::string dispatch(std::string_view orderId) override;
};

2.3The Mandatory Virtual Destructor (What Leaks Without It)

If you delete a derived object through a pointer to its base class (std::unique_ptr<DeliveryPartner> p = std::make_unique<BikeDeliveryPartner>(...);), the destructor must be declared virtual in the base class.

Without virtual ~DeliveryPartner() = default;, the runtime invokes only ~DeliveryPartner(). The derived destructor ~BikeDeliveryPartner() is completely skipped! All members owned by the derived class (such as std::string riderName_, open network sockets, mutex locks, or database connections) leak silently, invoking undefined behavior according to the C++ standard.

C++23 snippet
// ALWAYS include in any class with virtual methods:
virtual ~DeliveryPartner() = default;

2.4Under the Hood: The vtable and vptr Mental Model

When a class declares or inherits at least one virtual function, the compiler builds a static array of function pointers called the virtual method table (vtable). Every instance of that class contains an invisible pointer (vptr) placed at byte offset zero.

When executing partner->estimateEtaMinutes(km), the generated assembly performs:

  1. Read partner->vptr
  2. Index slot 1 in the vtable (vtable[1])
  3. Indirectly jump to the resolved function pointer. This involves one extra pointer dereference (a few nanoseconds), an imperceptible cost for high-level business routing.
C++23 snippet
// Conceptual assembly for partner->dispatch(orderId):
// (partner->__vptr[2])(partner, orderId);
03

UML class structure

BikeDeliveryPartner and DroneDeliveryPartner implement DeliveryPartner via virtual dynamic dispatch.

«abstract»
DeliveryPartner
Base contract with virtual destructor
  • + ~DeliveryPartner() = default
  • + partnerType(): string = 0
  • + estimateEtaMinutes(km: int): int = 0
  • + dispatch(orderId: string_view): string = 0
Used by:BikeDeliveryPartner(▲)DroneDeliveryPartner(▲)
BikeDeliveryPartner
final courier implementation
▲ inheritsDeliveryPartner
  • - riderName_: string
  • - bikePlate_: string
  • + partnerType(): string override
  • + estimateEtaMinutes(km: int): int override
  • + dispatch(orderId: string_view): string override
DroneDeliveryPartner
final aerial drone implementation
▲ inheritsDeliveryPartner
  • - droneId_: string
  • - batteryPct_: int
  • + partnerType(): string override
  • + estimateEtaMinutes(km: int): int override
  • + dispatch(orderId: string_view): string override
Relationships (UML arrows)
BikeDeliveryPartner──▷DeliveryPartnerinherits
DroneDeliveryPartner──▷DeliveryPartnerinherits
04

Runnable C++23 example

What the demo shows
  1. 1.

    Abstract Base Definition: DeliveryPartner declares pure virtual methods (= 0) for dispatching and ETA estimation, plus a default virtual destructor.

  2. 2.

    Concrete Subclasses: BikeDeliveryPartner and DroneDeliveryPartner override all virtual functions and are marked final.

  3. 3.

    Polymorphic Invocation: dispatchFleetOrder() takes a DeliveryPartner& reference. The runtime resolves the exact member function through the object's vtable.

  4. 4.

    Safe Destruction: Storing instances in std::vector<std::unique_ptr<DeliveryPartner>> ensures that destroying the vector polymorphically cleans up both bike and drone members.

Foundation Demo · demo.cpp
#include <format>
#include <memory>
#include <print>
#include <string>
#include <string_view>
#include <vector>

// Abstract polymorphic base class representing a delivery fleet partner.
// MANDATORY: Polymorphic base classes must define a virtual destructor!
class DeliveryPartner {
public:
    virtual ~DeliveryPartner() = default;

    [[nodiscard]] virtual std::string partnerType() const = 0;
    [[nodiscard]] virtual int estimateEtaMinutes(int distanceKm) const = 0;
    [[nodiscard]] virtual std::string dispatch(std::string_view orderId) = 0;
};

// Concrete derived class: Bike Courier.
// Marked 'final' so compiler can devirtualize calls when concrete type is known.
class BikeDeliveryPartner final : public DeliveryPartner {
private:
    std::string riderName_;
    std::string bikePlate_;

public:
    BikeDeliveryPartner(std::string riderName, std::string bikePlate)
        : riderName_(std::move(riderName)), bikePlate_(std::move(bikePlate)) {}

    [[nodiscard]] std::string partnerType() const override {
        return "Bike Courier";
    }

    [[nodiscard]] int estimateEtaMinutes(int distanceKm) const override {
        // Base: 5 mins prep + 3 mins per km in city traffic
        return 5 + (distanceKm * 3);
    }

    [[nodiscard]] std::string dispatch(std::string_view orderId) override {
        return std::format("[Bike] Assigned rider {} ({}) to order {}",
                           riderName_, bikePlate_, orderId);
    }

    [[nodiscard]] const std::string& riderName() const noexcept {
        return riderName_;
    }
};

// Concrete derived class: Autonomous Delivery Drone.
class DroneDeliveryPartner final : public DeliveryPartner {
private:
    std::string droneId_;
    int batteryPct_;

public:
    DroneDeliveryPartner(std::string droneId, int batteryPct)
        : droneId_(std::move(droneId)), batteryPct_(batteryPct) {}

    [[nodiscard]] std::string partnerType() const override {
        return "Autonomous Drone";
    }

    [[nodiscard]] int estimateEtaMinutes(int distanceKm) const override {
        // Direct flight: 3 mins prep + 1 min per km
        return 3 + (distanceKm * 1);
    }

    [[nodiscard]] std::string dispatch(std::string_view orderId) override {
        return std::format("[Drone] Launched drone {} (battery {}%) for order {}",
                           droneId_, batteryPct_, orderId);
    }

    [[nodiscard]] int batteryPct() const noexcept {
        return batteryPct_;
    }
};

// Polymorphic Dispatcher function: talks strictly to base interface references.
void dispatchFleetOrder(DeliveryPartner& partner, std::string_view orderId, int distanceKm) {
    int eta = partner.estimateEtaMinutes(distanceKm);
    std::string status = partner.dispatch(orderId);
    std::println("{}: ETA {} mins -> {}", partner.partnerType(), eta, status);
}

int main() {
    std::vector<std::unique_ptr<DeliveryPartner>> fleet;
    fleet.push_back(std::make_unique<BikeDeliveryPartner>("Rohan", "MH-12-AB-3421"));
    fleet.push_back(std::make_unique<DroneDeliveryPartner>("DRN-902", 88));

    std::println("--- Dispatching Order ORD-501 (3 km) ---");
    for (const auto& partner : fleet) {
        dispatchFleetOrder(*partner, "ORD-501", 3);
    }

    return 0;
}
Program output (stdout)
--- Dispatching Order ORD-501 (3 km) ---
Bike Courier: ETA 14 mins -> [Bike] Assigned rider Rohan (MH-12-AB-3421) to order ORD-501
Autonomous Drone: ETA 6 mins -> [Drone] Launched drone DRN-902 (battery 88%) for order ORD-501
05

Modern C++ note

Modern Devirtualization with override and finalC++20
What it replaces

Raw unannotated virtual functions prone to signature drift and missed compiler optimizations.

Why it is better

Tagging leaf classes with final allows modern compilers like GCC and Clang to perform whole-program devirtualization. When the compiler proves no further subclasses can exist, it removes the vtable pointer lookup and turns the call into a direct jump or inlined body.

When classic virtual wins

Extensible framework base classes designed to allow downstream plugin consumers to provide third-party overrides.

06

Rules of thumb

  • Always provide a virtual ~Base() = default; in every class that declares at least one virtual function.

  • Always append override to every virtual member function in derived classes to catch signature mismatches at compile time.

  • Mark leaf derived classes as final so the compiler can devirtualize method calls and optimize away vtable lookups.

  • Never pass polymorphic objects by value—slicing strips the derived part and destroys dynamic dispatch.

07

Don't make this mistake

Non-virtual destructor on a polymorphic base class
✗ class Base { public: virtual void run() = 0; // Missing virtual destructor! }; Base* b = new Derived(); delete b; // LEAKS Derived members, undefined behavior!
✓ class Base { public: virtual ~Base() = default; virtual void run() = 0; };

Deleting an object through a base pointer without a virtual destructor only calls ~Base(). Derived class heap memory, file handles, and strings leak silently.

Accidental object slicing by passing by value
✗ void dispatch(DeliveryPartner partner) { partner.dispatch("ORD"); }
✓ void dispatch(DeliveryPartner& partner) { partner.dispatch("ORD"); }

Passing by value copies only the DeliveryPartner base sub-object, discarding the derived BikeDeliveryPartner state and resetting the vtable pointer.

Calling virtual functions inside constructors or destructors
✗ class Base { public: Base() { init(); } // Virtual call in ctor! virtual void init() = 0; };
✓ class Base { public: Base() = default; void setup() { init(); } // Call after full construction. };

During Base construction, the derived object has not yet been built; its vptr points to Base's vtable. Calling a pure virtual function here crashes with pure virtual method called.

08

Quick interview check

09

Mark your progress

Systems Integration

Related C++ Systems & Algorithmic Foundations

Deep dive into underlying layers