Concepts / Virtual Functions and Run-Time Polymorphism

Virtual Functions and Run-Time Polymorphism

The common belief that virtual functions are slow and rarely needed mixes up a cost with a purpose; the source ties their use to a choice that must be made at run time.

  • Programming

A Belief Worth Testing

Many learners arrive at C++ with a simple rule in mind: virtual functions are a slow, specialized feature that belongs only in certain kinds of software, and a careful programmer avoids them when performance matters. This belief treats a virtual function as a cost to be paid, and it treats using one as a matter of taste rather than design. This article tests that belief against the historical account of how C++ acquired its features. That history shows that the people who added virtual functions were answering a specific question, not endorsing a style.

The article follows one running question. Suppose a drawing program keeps a collection of shapes, and every shape must be rotated by the same angle. The collection does not know whether each element is a particular kind of shape, and the code that rotates them does not need to know either. When the loop calls rotate on each element, which rotate actually runs?

Where the Belief Goes Wrong

The belief has two parts: that virtual functions are slow, and that they are rarely needed. The first part describes a real property of the mechanism. A virtual call is an indirect call, so the program must look up which implementation to use while it runs. The second part describes a purpose, and the belief does not address that purpose at all. The historical account separates the two. A cost is something a feature charges you. A purpose is the problem the feature solves. Judging a feature only by its cost leaves out the question of whether the problem exists in your program.

The early history shows that both camps of skeptics made the same error. Programmers from systems backgrounds viewed indirect function calls with suspicion, and programmers who had used other object-oriented languages found it hard to believe that virtual functions could be fast enough for systems code. Both groups focused on the cost of the mechanism. Neither group asked what the mechanism was for.

The Shape Collection Question

The rotation function below is written entirely in terms of Shape. It never mentions a particular shape, yet it is meant to rotate every shape it is given. Whatever it does with p, it does through the pointer, so the outcome depends on what p actually points to when the call happens. The code below is an illustrative example written for this article to match the scenario described above. It is not a listing from the source.

cpp
Output
Circle rotate by 90
Square rotate by 90
Notes on the code
  • Shape declares rotate as virtual, so derived classes may supply their own version.
  • Circle and Square each provide their own rotate.
  • rotate_all takes pointers to Shape and never names Circle or Square.
  • The call p->rotate(angle) is the single line that performs the rotation on each pass.

What do you think happens?

On the second pass, when p holds the address of the Square object, which rotate runs?

  • Shape::rotate, because the loop only knows about Shape

  • Square::rotate, because p refers to a Square object at run time

  • Circle::rotate, because it was the previous call

Reveal answer
Answer:

Square::rotate, because p refers to a Square object at run time

The text of the loop never changes, but the object p points to does. The call p->rotate(angle) reaches the version belonging to whichever object p refers to while the program runs.

Declaring a Virtual Function

The mechanism starts in the base class. A virtual function is declared there, and the declaration names the operation that derived classes are allowed to replace. In C++ the declaration is marked with the virtual keyword in the base class. A virtual function is therefore declared in a base class and can be overridden by derived classes.

virtualmarks the function asreplaceable by derivedclassesvoidreturn typerotatename of the operation(double angle)parameter list the overridemust match
Which parts of the base-class declaration does a derived class's override have to match?

A declaration in the base class says what can be called. It does not decide how a particular derived object carries out that call. A derived class supplies its own version when it needs different behavior, and the virtual declaration is what lets the call reach that version through a base-class pointer. The diagram names the parts of the declaration that a derived version has to line up with. If the name or parameter list differs, the derived function is a different function and does not take the place of the base version. The learner's first check on a surprising result is therefore whether the override really matches.

A derived class is a new class created on top of an existing one. To define a particular shape, you name its base class and then specify its own properties, including any virtual functions it provides. The derived class inherits the properties of its base class, so it starts from what Shape already offers and then adds or changes what makes it specific.

cpp
Notes on the code
  • The name matches, but the parameter type does not.
  • Calls made through a Shape pointer cannot reach this function.

Dispatch Through a Base-Class Pointer

Runtime polymorphism is the ability of an object to take on multiple forms depending on context, and virtual functions are how it is achieved. Calling a virtual function through a base-class pointer reaches the version belonging to the object the pointer refers to at run time. The same expression, p->rotate(angle), produces different behavior depending on the object it reaches while the program is running. The diagram shows the two iterations of the rotate_all loop in the example above, with each call routed to a different derived body.

rotate_all loopPointer pdeclared as Shape*Circle objectprovides Circle::rotateSquare objectprovides Square::rotateiteration 1: p holds Circlereaches Circle::rotateiteration 2: p holds Squarereaches Square::rotate
When p holds a Circle and then a Square, which function body runs for each call?

The static type of p never changes. It is always Shape*. What changes is the object p points to, and the object's own class determines which rotate body runs. The variable's declared type is one fact, and the object's actual type is another. Virtual dispatch uses the second fact.

Tracing the Loop Step by Step

When a rotation result is not what you expected, the useful question is where the behavior first departed from your mental model. Trace the loop one element at a time and, for each pass, write down three things: the object p currently points to, the class that object belongs to, and the rotate that the call reaches. The first pass where your expectation and the actual class disagree is the failure location.

Consider a collection of three shapes: a Circle, a Square, and a Triangle. The Triangle's author wrote rotate(int steps), as in the earlier listing, instead of rotate(double angle). Suppose you expected every element to print a derived message. The trace below shows exactly where the output departs.

Pass 1Circle, Circle::rotate, asexpectedPass 2Square, Square::rotate, asexpectedPass 3Triangle, Shape::rotate,departs from expectationnext elementnext element
At which pass does the output first differ from the expected derived message?
Worked example

Finding the first departure from expectation

Shapes holds a Circle, a Square, and a Triangle. Each pass should print a message naming the derived class. Find the first pass where the output does not match.

  1. 1
    Pass 1

    p points to a Circle. Its class is Circle. The call reaches Circle::rotate, which prints Circle rotate by 90. This matches the expectation.

  2. 2
    Pass 2

    p points to a Square. Its class is Square. The call reaches Square::rotate, which prints Square rotate by 90. This matches the expectation.

  3. 3
    Pass 3

    p points to a Triangle. Its class is Triangle. The call p->rotate(90.0) looks for a function taking a double through a Shape pointer. Triangle's rotate takes an int, so it does not replace the base version. The call reaches Shape::rotate, which prints Shape rotate by 90. The class is Triangle, but the function is from Shape. This is the first mismatch.

Result

The first departure occurs on pass 3. The Triangle object is correctly identified, but the derived rotate was never reached, because its signature does not match the base declaration.

Mistakes When Tracing Dispatch

The following errors appear most often when learners reason about a loop over base-class pointers. Each one confuses the variable's declared type with the object's actual type, or treats a mismatched function as an override.

  • Common mistake

    Judging which function runs by the type of the loop variable

    What it looks like

    Seeing Shape* p and concluding that Shape::rotate always runs.

    Why it fails

    The call is dispatched according to the object p points to at run time, not the declared pointer type.

    Do this instead

    For each pass, name the object p currently refers to and find its class before choosing the function.

  • Common mistake

    Assuming any function with the same name is an override

    What it looks like

    Writing rotate(int steps) in Triangle and expecting a call through Shape* to reach it.

    Why it fails

    The derived function must match the base virtual declaration. A different parameter list defines a different function.

    Do this instead

    Compare the name, return type, and parameters with the base declaration. Use the override marker so the compiler checks the match.

  • Common mistake

    Treating virtual as a reason to make every function virtual

    What it looks like

    Marking a helper that always does the same work as virtual.

    Why it fails

    The choice should follow the problem. A virtual call is the right expression only where a run-time choice is genuinely needed.

    Do this instead

    Ask whether the correct implementation can be fixed when the code is written. If it can, a virtual call is not the matching design.

  • Common mistake

    Believing the loop itself changes when the object type changes; the loop text is identical on every pass

The Run-Time Choice Rule

The historical account gives a rule that separates the cost of an indirect call from the purpose of letting an object's actual type pick the implementation. The correct use of a virtual function is decided by the structure of the problem. If the right implementation can be fixed when the code is written, a virtual call is not the expression that matches the problem. If the right implementation depends on information available only while the program runs, a virtual call is exactly what expresses that need.

Applied to the shape example, the rule gives a clear answer. Every element of the collection may be a different kind of shape, and the code that rotates them does not know which kind it will meet. The correct implementation depends on information that exists only at run time, so a virtual rotate is the right expression. Had every element been known to be a Circle at compile time, a virtual call would add cost without adding the flexibility it exists to provide.

Templates and the Three Goals

The same historical account shows that the early designers faced trade-offs elsewhere too. In the second half of the 1980s, language features were added in response to user comments. Two of the most significant were templates and exception handling, and both were still considered experimental when the standards effort began. Templates let a single definition describe a family of functions or classes, which is the core idea behind generic programming.

For templates, the designer had to choose among three goals: flexibility, efficiency, and early type checking. At that time, nobody knew how to achieve all three at once. Templates needed to compete with C-style code in demanding systems applications, so the choice favored flexibility and efficiency. The account reflects that, in retrospect, this was the correct choice, and the search for better type checking of templates has continued since then.

GoalPosition taken in early template design
FlexibilityPrioritized, so one definition could describe a family of functions or classes
EfficiencyPrioritized, so templates could compete with C-style code in systems work
Early type checkingNot fully achieved at the time, and the search for better template type checking continued

The three goals in the template trade-off, and how the early designers weighted them

Exceptions and Resource Management

The design of exceptions focused on three things: propagating an exception through multiple levels of calls, passing arbitrary information to an error handler, and integrating exceptions with resource management. The third point is the most distinctive. The approach uses local objects with destructors to represent and release resources. The designer called this idea, somewhat clumsily by his own account, Resource Acquisition Is Initialization, or RAII.

The central insight is that the lifetime of an object can be tied to the lifetime of a resource. When a local object is created, it acquires the resource. When the object's lifetime ends, its destructor releases the resource. Because a local object is cleaned up when control leaves its scope, the release happens on ordinary exit and also when an exception carries control away.

cpp
Output
acquire
release
handled
Notes on the code
  • The constructor acquires the resource when r is created.
  • The throw leaves work before it returns normally.
  • The destructor runs during unwinding, before the handler in main prints.

Multiple Inheritance and Interfaces

The last feature in this history is multiple inheritance. The designer generalized the inheritance mechanisms so that a class could have more than one base class. The account notes that this was considered difficult and controversial when it was introduced, and that the designer himself regarded it as far less important than templates or exceptions.

The later assessment is the important part for a learner. Multiple inheritance of abstract classes, often called interfaces, is now universal in languages that support static type checking and object-oriented programming. The feature that was once contested became most valuable in the form of pure interfaces, where a derived class promises to provide behavior described by more than one base.

Practice Tracing a Dispatch

MEDIUM

A vector of Shape pointers holds, in order, a Square, a Circle, and a Circle. Shape declares virtual void rotate(double angle), and Circle overrides it correctly. Square's rotate was written as void rotate(float angle). Trace rotate_all(shapes, 45.0). On which pass does the output first differ from a line naming the derived class, and what does that pass print?

Hints
  • For each pass, write down the object p points to and its class before you look at any function.
  • Compare the parameter type of Square's rotate with the base declaration.
  • The first mismatch may be on the first pass, even though later passes look normal.
EASY

Explain in two or three sentences why Circle's correct override can run for a Circle pointer even though the loop never names Circle. Use the words declared type and object type.

Hints
  • The declared type of p is Shape*.
  • Dispatch depends on the object p refers to while the program runs.

Key Takeaways

  • A virtual function is declared in a base class with the virtual keyword, and derived classes can override it to supply their own behavior.
  • A call through a base-class pointer runs the function belonging to the object the pointer refers to at run time, so one expression can produce different behavior for different objects.
  • Tracing a loop means recording, for each pass, the object pointed to, its class, and the function reached; the first mismatch with expectation locates the failure.
  • The run-time choice rule: use a virtual call when the right implementation depends on information available only while the program runs, not merely because the mechanism is sometimes considered slow.
  • The early history of templates, exceptions with RAII, and multiple inheritance shows that these features were chosen to answer specific design questions.

Related concepts

More concepts starting with V