Pointers to Base-Class Objects
A virtual function is declared in a base class and can be overridden by derived classes.
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?
Reveal 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.
Circle rotated by 30
Square rotated by 30
Circle rotated by 30- 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.
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
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.
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.
| Aspect | Virtual function | Non-virtual function |
|---|---|---|
| Where it is declared | In the base class, with the virtual keyword | In the base class, without the virtual keyword |
| Call through Shape* | Reaches the version for the object the pointer refers to at runtime | Runs the version selected by the pointer's declared type |
| Can derived classes replace it? | Yes, by supplying their own version | The declaration does not establish the runtime lookup described here |
| Role in runtime polymorphism | Achieves it, per the source | Not the mechanism the source describes |
Virtual versus non-virtual calls through a base-class pointer
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 likeReading p as a Shape* and concluding that every pass runs Shape's body in the example above.
Why it failsThe call reaches the version belonging to the object p refers to at runtime, which can be a Circle or a Square.
Do this insteadFor 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 likeSeeing that all three passes run the same rotation line and expecting the same output on every pass.
Why it failsThe choice is made at runtime, one element at a time, so the same text can produce different results.
Do this insteadTreat 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 likeAssuming the body written in Shape is what every shape uses because Shape declares rotate.
Why it failsThe base-class declaration says what can be called; a derived class supplies its own version when it needs different behavior.
Do this insteadCheck whether the object's class supplies its own version before assuming the base version runs.
Practice Tracing the Loop
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.