Concepts / Pointers to Base-Class Objects

Pointers to Base-Class Objects

A virtual function is declared in a base class and can be overridden by derived classes.

  • Programming

One Loop, Many Shapes

Imagine a drawing program that 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. The loop simply calls rotate on each element. This article asks a precise question about that situation: when the loop calls rotate on each element, which rotate actually runs?

Read the loop before running anything, and hold on to the single line that performs the rotation. Every element of the collection is a pointer to Shape, yet the objects those pointers refer to are specific shapes, each with its own rotate. The rest of this article is about what that one line does on each pass through the loop.

What do you think happens?

The loop text is identical for every element. When the rotation line runs for an element whose pointer refers to a Square object, which rotate body runs?

  • The rotate written in Shape, because the pointer type is Shape*

  • The rotate belonging to the Square object the pointer refers to at runtime

Reveal answer
Answer:

The rotate belonging to the Square object the pointer refers to at runtime.

The pointer's declared type is Shape*, but the call reaches the version belonging to the object the pointer actually refers to when the program runs. The choice is made per element, not fixed by the text of the loop.

Tracing the Loop Element by Element

The source's function is written entirely in terms of Shape. It never mentions a particular shape, and 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 points to at the moment the call happens. The example below is an original illustration of that pattern, using two derived shapes that each supply their own rotate.

cpp
Output
Circle rotated by 30
Square rotated by 30
Circle rotated by 30
Notes on the code
  • Shape* p: each element is declared as a pointer to the base class.
  • p->rotate(angle): the same expression on every pass; the body that runs depends on the object p refers to.
  • The vector holds the address of a Circle, then a Square, then a Circle again.
Pass 1Circle object, CirclerotatePass 2Square object, SquarerotatePass 3Circle object, Circlerotatenext elementnext element
As the loop visits each base-class pointer in turn, which object is each pointer pointing to at that step, and which rotate does the call reach?

Notice that the rotation line never changed. The text of the loop is the same on all three passes, yet each pass produced a different body. The choice is made as the program executes, one element at a time. That is what allows rotate_all to handle shapes it was never written to know about, as long as each shape supplies its own rotate.

Where the Virtual Declaration Lives

Definition

A virtual function is declared in a base class and can be overridden by derived classes. Runtime polymorphism, the ability of an object to take on multiple forms depending on context, is achieved through virtual functions.

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, as the Shape class in the example shows with its rotate function.

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.

Shapedeclares virtual rotateCirclesupplies its own rotateSquaresupplies its own rotatebase ofbase of
Where is the virtual function declared, and which derived classes replace it with their own version?

A derived class is a new class created on top of an existing one. To define a particular shape, you say that it is a Shape and then specify its particular 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.

Virtual Versus Non-Virtual Calls

The difference shows up when the same base-class pointer is used to call a function that is not declared virtual. The source's guarantee is specific to virtual functions: calling a virtual function through a base-class pointer reaches the version belonging to the object the pointer refers to at runtime. The table below sets the two cases side by side.

AspectVirtual functionNon-virtual function
Where it is declaredIn the base class, with the virtual keywordIn the base class, without the virtual keyword
Call through Shape*Reaches the version for the object the pointer refers to at runtimeRuns the version selected by the pointer's declared type
Can derived classes replace it?Yes, by supplying their own versionThe declaration does not establish the runtime lookup described here
Role in runtime polymorphismAchieves it, per the sourceNot the mechanism the source describes

Virtual versus non-virtual calls through a base-class pointer

p on pass 1Shape pointerCircle objectCircle rotateruns on pass 1p on pass 2Shape pointerSquare objectSquare rotateruns on pass 2points topoints torotate reachesrotate reaches
When one call like rotate is made through a Shape pointer, which function body actually runs for each object the pointer refers to?

Finding Where a Result Goes Wrong

When a rotation result is not what you expected, the useful question is where the behavior first departed from your mental model. Rather than rereading the whole program, trace the loop one element at a time. 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.

  • Common mistake

    Deciding which rotate runs from the pointer's declared type

    What it looks like

    Reading p as a Shape* and concluding that every pass runs Shape's body in the example above.

    Why it fails

    The call reaches the version belonging to the object p refers to at runtime, which can be a Circle or a Square.

    Do this instead

    For each pass, identify the object the pointer currently refers to, then find the rotate that belongs to that object's class.

  • Common mistake

    Assuming an identical loop body means identical behavior

    What it looks like

    Seeing that all three passes run the same rotation line and expecting the same output on every pass.

    Why it fails

    The choice is made at runtime, one element at a time, so the same text can produce different results.

    Do this instead

    Treat the loop text as fixed and the object each pointer refers to as the thing that varies.

  • Common mistake

    Treating the base-class declaration as the implementation that always runs

    What it looks like

    Assuming the body written in Shape is what every shape uses because Shape declares rotate.

    Why it fails

    The base-class declaration says what can be called; a derived class supplies its own version when it needs different behavior.

    Do this instead

    Check whether the object's class supplies its own version before assuming the base version runs.

Pass 1Circle, Circle rotatePass 2Square, Square rotatePass 3Circle, Circle rotatePass 4Square, Square rotatenext elementnext elementnext element
Suppose the vector is {&c, &s, &c, &s}. Your expectation is that each pass runs the same class as the element before it; which pass is the first to disagree?

Practice Tracing the Loop

EASY

Using the rotate_all function and the classes from the earlier example, suppose the vector is {&s, &c, &c} and the angle is 45.0. Write the output line for each pass, naming the object the pointer refers to and the rotate that runs.

Hints
  • Start with the first element. Which object does p refer to on pass 1, and which class does that object belong to?
  • Remember that the loop text never changes. Only the object the pointer refers to changes from pass to pass.
  • Expected result: Square rotated by 45, then Circle rotated by 45, then Circle rotated by 45.

Key Points to Retain

Key Takeaways

  • A virtual function is declared in a base class and can be overridden by derived classes.
  • Calling a virtual function through a base-class pointer reaches the version belonging to the object the pointer refers to at runtime.
  • Runtime polymorphism, the ability of an object to take on multiple forms depending on context, is achieved through virtual functions.
  • To locate a departure from expectation, trace the loop one pass at a time and record the object, its class, and the rotate that runs; the first mismatch is the failure location.

Related concepts

More concepts starting with P