Polymorphism

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.

Updated Fall 2024

1: Subtype Polymorphism
1.1 Not Started

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 a, b, and c below, and assume the Duck and Chicken classes are both derived from the Bird base class.

int main() {
  Bird b("Bonnie");
  Chicken c("Carlos");
  Duck d("Dinesh");
}

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.

Bird *bPtr = &b;
Chicken *cPtr = bPtr;
Bird *bPtr = &b;
bPtr = &d;
bPtr = &c;
Bird &bRef = c;
Chicken &cRef = bRef;
Bird &bRef = d;


Sample solution…

  Bird *bPtr = &b;
  Chicken *cPtr = bPtr;
  // error - downcast from Bird* to Chicken*
  Bird *bPtr = &b;
  bPtr = &d;
  bPtr = &c;
  // ok - as a Bird*, bPtr can point to any of the objects
  Bird &bRef = c;
  Chicken &cRef = bRef;
  // error - downcast from Bird& to Chicken&
  Bird &bRef = d;
  // ok - a Bird& is allowed to refer to a Duck



2: Static vs. Dynamic Binding
2.1 Not Started

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:

Bird *b_ptr;
// some code
// b_ptr ends up pointing at something
b_ptr->talk();

How do we ensure the correct version of talk() gets called, depending on the kind of bird that b_ptr ends up pointing at?

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 f1() function is non-virtual and the f2() function is virtual.

What number is printed by each of the following lines in main()?

class Fruit {
public:
  int f1() { return 1; }
  virtual int f2() { return 2; }
};

class Citrus : public Fruit {
public:
  int f1() { return 3; }
  int f2() override { return 4; }
};

class Lemon : public Citrus {
public:
  int f1() { return 5; }
  int f2() override { return 6; }
};
int main() {
  Fruit fruit;
  Citrus citrus;
  Lemon lemon;
  Fruit *fPtr = &lemon;
  Citrus *cPtr = &citrus;

  int result = 0;
  cout << fruit.f2() << endl;  
  cout << citrus.f1() << endl; 
  cout << fPtr->f1() << endl;  
  cout << fPtr->f2() << endl;  
  cout << cPtr->f2() << endl;  
  cPtr = &lemon;
  cout << cPtr->f1() << endl;  
  cout << cPtr->f2() << endl;  
}


Sample solution…

class Fruit {
public:
  int f1() { return 1; }
  virtual int f2() { return 2; }
};

class Citrus : public Fruit {
public:
  int f1() { return 3; }
  int f2() override { return 4; }
};

class Lemon : public Citrus {
public:
  int f1() { return 5; }
  int f2() override { return 6; }
};
int main() {
  Fruit fruit;
  Citrus citrus;
  Lemon lemon;
  Fruit *fPtr = &lemon;
  Citrus *cPtr = &citrus;

  int result = 0;
  cout << fruit.f2() << endl;  // 2 - direct call on a Fruit
  cout << citrus.f1() << endl; // 3 - direct call on a Citrus
  cout << fPtr->f1() << endl;  // 1 - f1 non-virtual, fPtr is declared Fruit*
  cout << fPtr->f2() << endl;  // 6 - f2 virtual, fPtr points to a Lemon
  cout << cPtr->f2() << endl;  // 4 - f2 virtual, cPtr points to a Citrus
  cPtr = &lemon;
  cout << cPtr->f1() << endl;  // 3 - f1 non-virtual, cPtr is declared Citrus*
  cout << cPtr->f2() << endl;  // 6 - f2 virtual, CPtr now points to a Lemon
}



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 Not Started

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.

class Player {
  // EFFECTS: Returns a card from player's
  // hand, following the rules of euchre.
  virtual Card play_card();
};

class DerivedPlayer : public Player {
  // EFFECTS: Always returns the ace of clubs
  Card play_card() override;
};
class PPMReader {
  // EFFECTS: Reads an image from a stream
  // REQUIRES: The stream must contain
  // image data in PPM format. Pixels must
  // be separated by a single space
  // character.
  virtual Image read_ppm_image(istream &is);
};

class DerivedPPMReader : public PPMReader {
  // EFFECTS: Reads an image from a stream
  // REQUIRES: The stream must contain
  // image data in PPM format. Pixels may be
  // separated by any kind of whitespace.
  Image read_ppm_image(istream &is) override;
};
class Unicorn {
  // EFFECTS: The unicorn fires a laser.
  // Returns the power of the laser beam,
  // which is at least 100kw.
  virtual double fire_laser_beam();
};

class DerivedUnicorn : public Unicorn {
  // EFFECTS: The unicorn fires a massive
  // laser. Returns the power of the laser
  // beam, which is over 9000kw.
  double fire_laser_beam() override;
};


You're welcome to check your solution with this walkthrough video:


You've reached the end of this lecture! Your work on any exercises will be saved if you re-open this page in the same web browser.

Participation Credit
Make sure to sign in to the page, complete each of the exercises, and double check the participation indicator at the top left of this page to ensure you've earned credit.