UPI Study › Courses › C++ Programming › Free lessons › Lesson 11.5 Free with a free account
How Polymorphism Enables Flexible Code
C++ Programming · Chapter 11: Inheritance and PolymorphismSection 11.5 · lesson 82 of 84video coming soon
← Why Encapsulation Strengthens DesignNext lesson: Hand-Crafting Linked Lists →
In this lesson
One call, different responses.
- Make a function virtual, and say what decides which runs
- Sketch how a virtual call is dispatched, and its cost
- Use pure virtual functions, abstract types and virtual destructors
- Avoid slicing; operate on a mixed collection with one loop
Transcript
Everything said in the video, section by section (1,649 words). Read along, or search it.
Opening Problem
A Mixed Collection of Shapes
Picture a drawing program with circles, squares and triangles, all in one list. You loop over it, and call draw on each one. You'd want each shape to draw itself. Inheritance so far has let a derived type reuse and extend a base. What it hasn't done is let a program hold a mixed collection, and treat every item the same way, while each behaves as itself. This section closes that gap, with one keyword, and one requirement about how objects are held. There are also a few consequences that must be learned together, because each one bites when the others are forgotten.
The Gap
Base Pointer Runs the Base Draw
Derived types can already replace a base function. What they can't yet do is have that replacement chosen when the object is handled as a base. And being handled as a base is exactly what makes a mixed collection possible. Here, circle redefines draw. A shape pointer holds the address of a circle, and draw is called through it. It runs shape's draw, which isn't what was wanted. The object really is a circle. But the decision was made when the program was built, using the only thing available then: the pointer's type.
Comparison
Deciding the Call Two Ways
Here's the same call, decided two ways. Without virtual, the call is decided when the program is built, from the pointer. The circle object is never consulted, and shape's draw runs. With virtual, the call is decided while the program runs, from the object itself. The object is asked what it is, and circle's draw runs. Adding one keyword moves the decision from the pointer to the object.
Core Syntax
Virtual, Override, and Three Rules
Here's that keyword. The base declares draw as virtual, meaning it may be replaced by derived types. It also declares a virtual destructor, which isn't optional, as we'll see shortly. Circle's draw is marked override, and replaces the base version. Now the call through the shape pointer runs circle's draw. Three rules govern this. The base declares the function virtual. The derived version must have the same signature: same name, same parameters, same constness. Otherwise it becomes a different function, not a replacement. And once a function is virtual, it stays virtual in every type below, whether or not the word is repeated.
Best Practice
Let the Compiler Check Signatures
Write override. It adds no behavior, but a mistyped name or missing const becomes an error at that line. Without it, a slightly different signature still builds, at most with a warning, and the wrong function runs forever. With it, the compiler checks you, and points at the mismatch: const against no const.
Summary Rule
Pointer Type Versus Object Type
So here's what decides which function runs. A non-virtual call is decided when the program is built, from the type of the pointer or reference. A virtual call is decided as the program runs, from the type of the object. So through a base pointer to a derived object, the non-virtual call runs the base version, and the virtual call runs the derived version. Adding a new derived type means editing every place that switches on type. With virtual calls, nothing calling through the base changes. The cost of a virtual call is one extra step per call, and one hidden pointer per object.
Requirement
Pointers and References Enable Dispatch
Dynamic dispatch needs an object whose real type may differ, from the type of the name used to reach it. A pointer and a reference both allow that. Through either one, the circle's draw runs. A plain variable doesn't, because a variable of type shape is a shape, and nothing else. Assigning a circle into one does something quite different, which we'll come to.
Under the Hood
Hidden Tables of Function Addresses
Here's how the choice is usually made, underneath. A type with virtual functions gets a hidden table of function addresses, one per type, with one entry per virtual function. Each object carries a hidden pointer to its own type's table. A virtual call reads that pointer, looks up the entry, and jumps there. So an object built as a circle carries the circle's table, and the call lands in the circle's code.
Cost
Seeing the Hidden Pointer in Size
The cost is one hidden pointer per object, and one extra read per call. You can see the pointer in the object's size. Two structs, identical except that one has a virtual function. On this 64-bit machine, the plain one takes four bytes, and the polymorphic one takes sixteen: the hidden pointer, plus padding. Both costs are small, and both are real, which is why functions aren't virtual by default.
Pure Virtual
Equals Zero Makes Shape Abstract
Sometimes a base has no sensible implementation to offer. There's no default way to draw a shape. Only circles and squares can be drawn. Writing equals zero declares the function pure virtual. Every derived type must supply it, and the base itself can no longer be instantiated. Declaring a plain shape is refused, because shape is abstract. But a shape pointer to a circle is fine. Pointers to it are the point.
Type Categories
Concrete, Abstract, and Interface Types
That gives three kinds of type. A concrete type implements every function, and can be instantiated, so it's used as objects, directly. An abstract type has at least one pure virtual function, and maybe some implemented ones and data. It's used only as a base, a partial implementation that derived types complete. An interface-style type has nothing but pure virtual functions and a virtual destructor. It's a pure statement of what an implementer must provide. An abstract base is a promise the compiler enforces. Any type that fails to supply a pure virtual function is itself abstract. So the obligation can't be forgotten, only passed further down.
Common Mistake
Non-Virtual Destructors Lose Resources
If objects are deleted through base pointers, and with polymorphism they usually are, the destructor obeys the same rule as every other function. A non-virtual destructor is chosen by the pointer's type. Only the shape destructor runs. Whatever the circle owned, whether memory, an open file, or a connection, is silently lost. And nothing reports it. The language calls this undefined behavior, so this program is never run. The compiler can warn, though, and here is its real warning.
The Fix
Virtual Destructors Destroy Whole Objects
With a virtual destructor, the circle's destructor runs, then the shape's. The object is destroyed as what it actually is. So the rule is: if a type has any virtual function, give it a virtual destructor. It costs nothing, because the type already carries the hidden pointer.
Common Mistake
Slicing Discards the Derived Part
Assigning a derived object into a base variable doesn't fail, and doesn't warn. It copies the base part, and discards everything else, including the object's identity as a derived type. So the sliced copy runs shape's draw. It's genuinely a shape now. The second case catches people. A function taking a shape by value slices every argument on the way in, and nothing at the call looks unusual. The remedy is one character: take a const shape reference, and the object arrives whole. Make it a habit. Polymorphic objects are passed and held by reference or pointer, never by value.
Contrast
Overloading Versus Virtual Dispatch
Overloading already gave one name several meanings. Virtual dispatch gives one call several behaviors. Both are called polymorphism, and they resolve at opposite ends of a program's life. Here, describe is overloaded, and the compiler picks the version from the argument's type. But a-arrow-draw and b-arrow-draw are the same call, and each runs its own object's version.
Comparison
Build Time Versus Run Time Resolution
Overloading is resolved when the program is built, chosen by the number and types of the arguments, with no run-time cost. Virtual calls are resolved each time the call is reached, chosen by the object's actual type, for one table lookup per call. And to extend them, overloading needs a new overload and a rebuild of every caller. Virtual dispatch needs a new derived type, and existing calling code is untouched.
Payoff
Adding a Hexagon Without Editing
Everything so far exists for this. A vector of unique pointers to shape can hold a circle, a square, and a triangle. One loop draws each shape, and adds up their areas, without knowing or asking what any of them is. Now add a hexagon. It derives from shape, supplies draw and area, and is pushed into the same container. The loop doesn't change, not by one character, because it never mentioned any particular shape. Run it again, and the hexagon draws itself and adds its area. That's the whole return on the machinery. New behavior is added by writing a new type, not by editing existing code.
Design Choice
Why Unique Pointers Hold Shapes
Why unique pointer, rather than plain pointers? A container of raw shape pointers works, and leaves you owing a delete for every element, on every path out. Holding unique pointers instead means each object is released when the container is, automatically. And the virtual destructor is what makes that correct. A unique pointer deletes through a shape pointer, which is exactly the case a non-virtual destructor gets wrong.
Key Terms
Polymorphism Vocabulary Review
Key terms. Polymorphism is one call producing different behavior, according to what the object actually is. Dynamic binding chooses the function at the moment of the call, from the object's real type. Static binding chooses when the program is built, from the type of the name used. A pure virtual function is declared equals zero, with no base version. A type with at least one is abstract, and objects of it can't be created. Object slicing is copying a derived object into a base variable, which discards the derived part. And the dispatch table is the per-type table of function addresses that a virtual call consults.
Quick Check
Which Draw Runs Without Const?
Quick check. The base draw is virtual and const. Circle declares draw without const, and without override. Called through the shape pointer, which draw runs? Pause and predict. Shape's draw. Without const, the signatures differ, so circle's draw is a separate function, not a replacement. Writing override would have caught it at build time.
Key Takeaways
Five Rules for Polymorphic Code
To summarize. Virtual moves the decision from the pointer to the object. Match signatures exactly, and write override so the compiler checks. Pure virtual functions make abstract bases, enforced by the compiler. Any virtual function means a virtual destructor. And hold polymorphic objects by pointer or reference, never by value.
About C++ Programming at UPI Study
C++ Programming (Computer Science 242) is a self-paced online course of 84 video lessons in 11 chapters, from your first program to decisions, loops, arrays, functions, classes, files, pointers and inheritance. It carries a NCCRS credit recommendation for 3 semester credits.
Every lesson transcript is free on pages like this one, and the videos are free with a free UPI Study account. Enrolling adds a practice quiz after each lesson, the graded work and final exam that earn the credit, lifetime access, and an official transcript on completion. UPI Study has helped over 130,000 students with college credits. ACE and NCCRS have over 2,100 cooperating colleges that will consider accepting credits; the final decision is with the universities.
Enroll now → Course details Download the syllabus Every course with PRO →
Why take it here
Why students take C++ Programming with UPI Study
Where the credit goes
Universities that work with NCCRS
C++ Programming carries a NCCRS credit recommendation. These universities work with NCCRS; some are UPI Study's signed partners. ACE and NCCRS have over 2,100 cooperating colleges that will consider accepting credits; the final decision is with the universities.
Thomas Edison State UniversitySigned partner
University of the PeopleSigned partner
Charter Oak State CollegeSigned partner
Strayer UniversitySigned partner
Columbia Southern UniversitySigned partner
Indiana TechSigned partner
Penn StateWorks with NCCRS
Southern New Hampshire UniversityWorks with NCCRS
SUNYWorks with NCCRS
+many more cooperating universitiesFind my college →Find my college → Not listed? Ask Athena in the corner of this page, or book a 15-minute call.
FAQ
Questions students ask us
About How Polymorphism Enables Flexible Code
What does the virtual keyword do in C++, and what decides which function runs?
Why should I write override on a derived class function in C++?
Why does a C++ base class need a virtual destructor?
What is object slicing in C++, and how do I avoid it?
About the C++ Programming course
How many college credits is C++ Programming?
How much does C++ Programming cost and what is included?
Which universities accept UPI Study credits?
How long does C++ Programming take?
How are the exams done?
How do my credits reach my college?
Ready for the full course? Earn 3 credits in C++ Programming.
84 video lessons, a quiz after each one, and the graded work that earns the credit. Recommended for credit by NCCRS.
3 credits · NCCRSEnroll →