This lecture explores polymorphism, which makes our code more flexible and enables even just a single line of code to potentially do many different things in different contexts or situations. It's one of the most powerful concepts in programming.
We'll specifically focus on subtype polymorphism today.
1: Subtype Polymorphism
1.1
First, a brief intro to the many forms of polymorphism: To enable subtype polymorphism specifically, we'll need a way for a single base class variable to potentially work with any object of any derived class. As usual, the answer is more pointers :).
1.1 Exercise: Upcasts and Downcasts
Consider the variables
Consider each of the following code snippets. Each involves upcasts or downcasts, some with pointers and some with references (note that the rules for references are the same as for pointers - upcasts are safe but downcasts are not!). If the compiler would allow the code, write "ok". Otherwise, write "error" and a brief explanation of the problem.
Sample solution…
|
2: Static vs. Dynamic Binding
2.1
We've now got a way (i.e. using pointers/references) to have a polymorphic variable that can potentially point to any type derived from a particular base, but there's still something missing. Let's say we have this code:
How do we ensure the correct version of The answer is to use virtual functions to ensure dynamic binding of the function call. A common pattern for type hierarchies is that the base class will define a virtual function with the expectation that derived classes may provide their own implementations that override the original behavior of that function (since dynamic binding ensures the more specific version is called).
2.1 Exercise: Virtual vs. Non-Virtual Functions
Shown below are a hierarchy of fruit-based classes and a main function that makes member function calls on a variety of fruit objects and pointers. Note that the What number is printed by each of the following lines in
Sample solution…
|
3: Pure Virtual Functions
If a "default" implementation doesn't make sense for the base class, we can also opt to define the function there as pure virtual, meaning that it doesn't have any implementation. |
4: Interface Inheritance
It turns out that inheriting interfaces is just as important (if not more) than inheriting implementations. Here's why: |
5: Factory Functions
The last piece of the puzzle for polymorphic objects is where to create them. Ideally, we don't want client code to have to deal with creating specific derived class objects, so we provide a "factory function" that abstracts away that process. |
6: The Liskov Substitution Principle
6.1
Finally, we'll briefly describe the Liskov Substitution Principle, which formally qualifies whether or not a derived type is proper subtype and satifies everything expected of its base type.
6.1 Exercise: Liskov Substitution Principle
Consider each pair of base and derived classes below with comments that describe the behavior of a virtual/overridden function. Is the derived class a proper subtype according to the Liskov Substitution Principle? Explain your reasoning.
You're welcome to check your solution with this walkthrough video: |