Concepts / Virtual Functions and Dynamic Dispatch

Virtual Functions and Dynamic Dispatch

When the function is virtual, the program chooses the version that matches the actual object, not the type of the pointer or reference.

  • Programming

Why Shared Behavior Gets Duplicated

Programs often contain several kinds of objects that are related but not identical. A drawing program may have circles, squares, and triangles. A zoo simulator may have dogs, cats, and birds. Each kind has its own details, yet all of them share some common behavior, such as being drawn, being fed, or being named.

If the same shared code is written again for every kind, the program becomes full of duplication, and any change to the shared behavior must then be made in many places. C++ addresses this problem with classes arranged in a hierarchy. A common base class holds the shared description, and more specific derived classes add to or change that behavior. This article follows one question through that design: when code refers to an object only through its general base type, how does the program decide which specific version of a behavior to run?

Class Hierarchies from General to Specialized

Definition

A class hierarchy is a set of classes connected by inheritance, where a derived class is built on top of a base class. Each derived class is a more specific kind of the class above it. In a hierarchy, the top class is the most general, and the classes further down are more specialized.

Picture a drawing program in which every shape can be drawn. The base class Shape describes what all shapes have in common. The derived classes Circle, Square, and Triangle are each a more specific kind of Shape. Each one carries its own details, but the drawing behavior they share can sit in the base class instead of being copied into every derived class.

Shapeshared drawing behaviorCirclespecialized shapeSquarespecialized shapeTrianglespecialized shapeis a kind ofis a kind ofis a kind of
How are the general base class and the specialized derived classes arranged, and which shared behavior sits in the base class?

A hierarchy answers the question of what contains what. Every derived object also contains the base-class part, and it can add its own members on top of that base part. Because of this, a derived object can always be treated as an object of its base class, which is the foundation for everything that follows.

Tracing a Call Through a Base Pointer

Suppose code holds a Circle object but refers to it through a Shape reference or pointer, and then calls draw. The code only knows the base type, yet the object underneath is a Circle. The question is which version of draw should run. Following the call step by step shows where that decision is made.

Caller codeBase-class pointerCircle objectCircle drawcalls draw()refers to actual objectruns matching version
When a call goes through a base-class pointer, what steps happen at runtime to reach the derived class's version of the function?

Read the diagram from top to bottom. The caller uses the base-class pointer and asks for draw. The pointer leads to the object that actually exists in memory, which is the Circle. Only then does the program select the Circle version of draw. The key step is the third arrow: the choice is tied to the actual object, not to the pointer's declared type. That is why the behavior is described as dynamic.

How a Virtual Function Picks the Right Version

Definition

A virtual function is a member function declared in a base class that a derived class may replace. Virtual functions matter when a derived object is held through a base-class pointer or reference, because the question is then which version of the function should run.

When the function is virtual, the program chooses the version that matches the actual object, not the type of the pointer or reference. This choice is made while the program runs. The example below uses a Shape base class with a virtual draw function, a generic version in the base, and a Circle and a Square that each replace it.

cpp
Output
Drawing a circle
Drawing a square
Notes on the code
  • draw is declared virtual in the base class Shape
  • render takes only a Shape reference, so it cannot know the exact type
  • Each call to s.draw() runs the version belonging to the object that was passed in

Notice that render was written once and only mentions Shape. It works correctly for circles, squares, and any other derived shape added later, without changing render itself. This is the practical gain: shared code can be written against the general base type while the specific behavior still comes from the derived object.

Explicit Overriding with the override Keyword

Explicit overriding means marking a derived-class function as replacing a base-class virtual function, using the override keyword. The marking is a request to the compiler to check the match. The check matters because a replacement function can fail to match without anyone noticing. A misspelled name or a different parameter list would silently create a new, unrelated function, leaving the base version in place.

cpp
Notes on the code
  • The base class declares draw() with no parameters
  • This function takes an int, so it does not truly match the base virtual function
  • Because of override, the compiler reports an error instead of accepting the mismatch

Moving Up and Down the Hierarchy

Hierarchy navigation is the work of moving through a hierarchy, either upward from a derived object to its base view or downward from a base reference to a more specific derived type. Upward movement happens naturally, because a derived object is already a base object. Downward movement needs an explicit check, since a base pointer might not point to the derived type you expect.

cpp
Notes on the code
  • The conversion to Shape* happens without any check, because a Circle is a Shape
  • dynamic_cast is the check that the source material names for moving downward
  • The null test guards against a base pointer that does not point to a Circle

Writing code that moves between these views carefully keeps the hierarchy's structure intact. Going up is safe by construction. Going down should always pass through a check, so that the program does not assume a more specific type it has not verified.

Resource Leaks When Deleting Through a Base Pointer

A resource leak happens when a program acquires something, such as memory, and never releases it. In a hierarchy this often goes wrong when an object is created as a derived type but destroyed through a base pointer. The example below gives the derived class its own dynamically allocated array, and then deletes it through a Base pointer.

cpp
Notes on the code
  • Derived allocates an array in its constructor and is supposed to release it in its destructor
  • The Base destructor is not declared virtual
  • The delete goes through a Base pointer, so the Derived cleanup is not run

The source material describes exactly this situation: if the base class lacks a virtual destructor, deleting through the base pointer does not run the derived class's cleanup, and the derived part's resources may never be released. In the code above, the line that releases the array never executes, so the array is leaked. The diagram compares the two cases.

Delete through basepointerDelete through basepointerBase cleanupVirtual basedestructorDerived cleanupskippedDerived cleanup runsBase cleanup runscalls base destructorno path to derivedcalls virtual destructorchain reaches derivedthen base
When a derived object is deleted through a base pointer, which destructors run, which resources are left behind, and how does a virtual destructor change that?

Virtual Destructors as the Fix

Declaring the base destructor virtual ensures the correct destructor chain runs. When the base destructor is virtual, deleting through a base pointer runs the derived class's cleanup first and then the base cleanup, so the derived resources are released. Only the base class declaration needs the change; the derived destructor is then part of the same chain.

cpp
Notes on the code
  • The only change from the leaking version is virtual in the Base destructor
  • override on the Derived destructor asks the compiler to confirm it replaces the base destructor

Mistakes Beginners Make with Hierarchies

  • Common mistake

    Leaving the base destructor non-virtual in a class meant to be deleted through a base pointer

    What it looks like
    class Base { public: ~Base() {} }; with a Derived that owns an array, then delete through a Base pointer
    Why it fails

    The derived cleanup is not run, so the derived resources may never be released

    Do this instead
    Declare the base destructor virtual, as in virtual ~Base() {}
  • Common mistake

    Expecting a derived replacement to take effect when its signature does not match the base function

    What it looks like

    A derived draw(int) intended to replace a base draw() with no parameters

    Why it fails

    The function with a different parameter list does not replace the base virtual function, so base-reference calls keep running the base version

    Do this instead

    Mark the replacement with override so the compiler reports the mismatch

  • Common mistake

    Assuming a base pointer always points to the derived type you expect and converting it downward without a check

    What it looks like

    Converting a Shape* straight to a Circle* and using it without testing the result

    Why it fails

    A base pointer might not point to a Circle at all, and downward movement needs an explicit check

    Do this instead

    Use dynamic_cast and test the result against nullptr before using it

  • Common mistake

    Putting shared behavior into every derived class instead of the base class, which duplicates code and forces edits in many places

Practice: Predict Which Version Runs

What do you think happens?

In the render example, render(c) is called with a Circle object, but render's parameter is a Shape reference. Which version of draw runs?

  • The generic Shape version, because the parameter is a Shape

  • The Circle version, because the object passed in is a Circle

Reveal answer
Answer:

The Circle version runs, and the program prints Drawing a circle

draw is virtual, so the program chooses the version that matches the actual object, not the reference's declared type.

MEDIUM

A class Base has a non-virtual destructor, and a class Derived that derives from it allocates memory in its constructor and frees it in its destructor. Explain in two or three sentences what goes wrong if you write Base* p = new Derived(); delete p; and state the one change that fixes it.

Hints
  • Ask which destructor the delete through the base pointer runs
  • Think about what happens to the derived part's resources

Key Takeaways

  • A class hierarchy connects a general base class to more specialized derived classes, so shared behavior can live in the base class instead of being duplicated.
  • A virtual function lets a call through a base-class pointer or reference run the version that matches the actual object, and this choice is made while the program runs.
  • The override keyword asks the compiler to confirm that a derived function really replaces a base virtual function, turning silent mismatches into errors.
  • Moving down a hierarchy needs a check such as dynamic_cast, and a virtual destructor in the base class prevents resource leaks when deleting through a base pointer.

Related concepts

More concepts starting with V