12.4. Actor 5: A Polymorphism Example

Time: 00:04:00 | Download: Large, Large (CC), Small | Streaming, Streaming (CC) | Slides (PDF)
Review

Two versions of Actor 5 demonstrate polymorphism. The first version begins with the Actor 4 example, and makes the display() function virtual, demonstrating basic polymorphism and the limitations of non-member functions. The second version adds authenticity by inserting instances of Person, Actor, and Star into a data structure named CList. A CList maintains an alphabetically ordered list of objects instantiated from Person and its subclasses. The order in which the list saves the objects may differ from the insertion order because it organizes them alphabetically, but polymorphism displays them correctly. The example presents some code fragments for each version in the discussion and provides links to the complete programs at the end of the section.

Basic Polymorphism: A Review

The chapter began with a list of five elements that classes and programs must provide to enable polymorphism:

  1. Inheritance
  2. Function overriding
  3. A pointer or reference variable (polymorphism cannot operate through an automatic or local variable)
  4. Upcasting
  5. One or more virtual functions

As a practicing computer scientist, no one will ask you to list these elements. However, reading and understanding the behavior of a program is an expected skill. In the case of polymorphism, you must recognize the presence or absence of the five elements in a program and understand their impact on it. As you study the following changes to the Actor program, compare the changes to the list of requirements. A program must implement all five features to activate polymorphism.

The Actor 4 example has five classes, but only three, Person, Actor, and Star are related by inheritance and participate in polymorphism. The remaining two, Address and Date, don't participate in polymorphism and remain unchanged from the earlier example. Each class defines a display function with a void return type and an empty parameter list, satisfying requirement 2. So, Actor 4 classes have the first two "ingredients" for polymorphism, and the "Automatic" parts of the driver satisfy requirements 3 and 4. However, we must modify the example to achieve all the requirements.

Simple Polymorphism And Non-Member Functions

Many of polymorphism's requirements are intrinsic to the problem and its solution (1 and 2) or depend on the client program (3 and 4). However, a superclass with an overridden function is responsible for the fifth and final requirement. The Actor 5 example makes the display functions in the Person, Actor, and Star classes virtual, satisfying requirement 5 and making display polymorphic.

virtual void display()
{
	cout << name << endl;
	addr.display();			// composition
	if (date != nullptr)		// display if available
		date->display();	// aggregation
}
Enabling polymorphism. Making the display function virtual satisfies requirement 5, activating polymorphism. Adding the "virtual" keyword is a deceptively simple step, belying the complex structures and operations it adds to a program. Fortunately, polymorphism hides this complexity, allowing us to use it without concern about its implementation. Requirements 1 and 2 imply that polymorphism only works when a program has two or more inheritance-related classes with at least one overridden function. The Actor 5 example makes the Person class display function virtual, automatically making the Actor and Star display functions virtual. Nevertheless, I typically add the keyword to the subclass's functions for clarity.
#include "Star.h"
#include <iostream>
#include <string>
using namespace std;

int main()
{
	Person* s2 = new Star("John Wayne", "Cranston Snort", 50000000, "123 Palm Springs", "California");

	s2->setDate(1960, 12, 25);
	s2->display();
	cout << endl;
	cout << *s2 << endl;

	return 0;
}
The client's contribution to polymorphism. As demonstrated by the Actor 5 main function, the client program often satisfies requirements 3 and 4. It also typically calls the virtual function, triggering the polymorphic behavior.
s2->display(); cout << *s2 << endl;
John Wayne
California, 123 Palm Springs
1960/12/25
Cranston Snort
5e+07
John Wayne
California, 123 Palm Springs
1960/12/25
 
 
(a)(b)
Polymorphic behavior. The Actor example has unrealistically maintained two output operations throughout its evolution - "real" programs typically choose one or the other - for two reasons. First, to demonstrate the distinctly different syntaxes required to use the display function and the overloaded inserter operator. The second reason is demonstrating the difference between polymorphic and non-polymorphic behavior. The display function is a member of each Actor class, while operator<< is a friend function and therefore cannot be a member. Please notice that this example instantiates a subclass (Star) and upcasts it to a superclass (Person).
  1. Adding the "virtual" keyword to the display function prototype satisfies all five polymorphism requirements. So, despite s2 being a Person pointer, the statement s-⁠>display(); runs the Star display function, printing the first three output lines. The example chains the display functions upwards from Star to Actor (printing the fourth line) and from Actor to Person (printing the fifth line). The virtual keyword in the Actor 5 display is the cause for the difference between the display and operator<< output.
  2. In contrast, C++ always implements the inserter as a non-member friend, which is non-polymorphic. Upcasting Star to Person in this example causes the inserter to print only the Person data. Compare the Actor 5 output shown here to the Actor 4 operator<< output.
When polymorphism is fully enabled, the function belonging to the instantiated object's class, not the pointer variable, runs. Alternatively, when polymorphism is not enabled, the function belonging to the pointer-variable's class runs. Therefore, the inserter function call runs the Person inserter, showing only the Person's name, address, and date, but none of the information stored in the subclass objects.

An Extended Polymorphism Example

The second, extended version presents polymorphism in a slightly more authentic setting using a dynamic data structure called a linked list. Data structures store data (often objects), organize them in some way, and provide a set of operations to maintain the list and access the data. Linked data structures consist of nodes containing the data and one or more pointers called links. The example uses a circularly linked list, one of the many linked list variations, formed by each node pointing or linked to the following node and the last node pointing to the first.

A circularly-linked list with data A logically empty list
A picture of a circularly-linked list formed by lining nodes together with pointers. Each list node, represented by a rectangle, is an object instantiated on the heap with new. Nodes contain data and a pointer to the next node in the list. A special node, called a list head or header, doesn't contain data but forms the interface between a program and the list. The list is circularly-linked because the pointer in the last node points back to the list head. A logically empty circularly-linked list consists of a header node without data whose link points to itself.
A circularly-linked list. The list head does not store any data but acts as a "handle" to manipulate or "hang on to" the list. The "Data" in each node is a pointer to an instance of the Person class. A logically empty list consists of a header node without any data nodes, avoiding the boundary conditions encountered when inserting the first data node in the list or removing the last data node.

Although library-grade data structures can generally store any data type, the list used in this demonstration is limited for simplicity, only storing instances of the Person class. There are many variations on linked lists, and this variation organizes the data alphabetically based on the person's name. This organization implies that the list can reorder the stored objects. So, the storage order is not necessarily the same as the insertion order, making a more interesting demonstration of polymorphism.

The second Actor 5 example implements the circularly linked list as a supplier class named CList. While the class's details are more appropriate for a data structures and algorithms course, it presents a straightforward public interface consisting of six functions, which we explore through a set of function prototypes:

CList()
Default Constructor builds a new, empty circularly-linked list.
~CList()
Destructor deallocates all list nodes; removing a comment in the function body also deletes the stored data.
Person* insert(Person* p)
Searches the list for a person matching p, returning it if a match is found. If the search doesn't find a match, the function creates a new node, storing p, linking it in the list alphabetically, and returning it.
Person* search(Person* key)
Searches the list for a person matching key, returning if found or turning nullptr otherwise.
Person* remove(Person* key)
Searches the list for a person matching key, removing it from the list if found. The function returns the matched and removed Person if found; otherwise it returns nullptr.
void list();
Calls the display function for each Person in the list, listing or printing all saved data.
CList member functions. This linked-list version stores instances of the Person class, organizing them alphabetically by their name, but disallows duplicates (i.e., people with the same name are not permitted). To search and order the list, three functions, insert, search, and remove, require the Person class to define a getter function named getName(). The Person class has three member variables: name, date, and addr (the person's address). However, operations comparing two Person objects only use the name. Two operations require a key, a partially-filled Person object storing only a name.

The second version only requires adding a getter function to the Person class.

string getName()
{
	return name;
}
A getter function for class Person. Three CList functions require access to the name stored in each Person object. However, the name member variable is private, necessitating a getter function.

The Actor 5 main creates a CList, adds three different kinds of Person objects to the list, and then prints all of the information for each object:

#include "Person.h"
#include "Actor.h"
#include "Star.h"
#include "CList.h"								// (a)
#include <iostream>
#include <string>
using namespace std;

int main()
{
	CList	people;								// (b)

	Star* s = new Star("John Wayne", "Cranston Snort", 50000000,		// (c)
			"123 Palm Springs", "California");
	s->setDate(1960, 12, 25);
	people.insert(s);

	Actor* a = new Actor("Dilbert", "Wally", "2401 Edvalson", "Ogden");	// (c)
	a->setDate(1961, 1, 1);
	people.insert(a);

	Person* p = new Person("Alice", "115 Elm", "Ogden");			// (c)
	p->setDate(1975, 5, 22);
	people.insert(p);

	people.list();								// (d)


	return 0;
}
Inserting data (instances of Person and its subclasses) into a list.
  1. Includes the CList header file so that the source code file "knows about" a CList.
  2. Creates and names the CList object; I named the CList people in this example.
  3. Creates new Person objects and inserts them into the list. Note that when the program instantiates Star and Actor objects, the insertion operation upcasts them to a Person.
  4. Prints all the information for each object in the list. The list function "walks" the list from the beginning to the end, calling each object's display function. The insert function can reorder the list's nodes to maintain the alphabetical ordering. Therefore, the list function lists the nodes in alphabetical rather than insertion order. Fortunately, polymorphism calls the correct display function for each object regardless of the order, demonstrating its usefulness.

Downloadable Code

Much of the Actor 5 demonstration code is copied or adapted from the Actor4.cpp example.

VersionViewDownloadComments
All Address.h Address.h Unchanged from Actor 4
Date.h Date.h
Person.h Person.h Added getName() (for CList); made display virtual
Actor.h Actor.h Unchanged from Actor 4
Star.h Star.h
1 Actor5.cpp Actor5.cpp Adapted from Actor4.cpp
2 CList.h CList.h
CList.cpp CList.cpp
Actor5_poly.cpp Actor5_poly.cpp