12.4. Actor 5: A Polymorphism Example

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

Actor 5 begins with the non-polymorphic Actor 4 example and takes it through two evolutionary steps. First, it makes the program polymorphic by making the display function virtual, demonstrating basic polymorphism and the limitations of non-member functions. The second step introduces a dynamically-organized data structure. The program inserts instances of Person, Actor, and Star into the structure, demonstrating that polymorphism can select and run the correct function even when an object's precise class-type is unknown at compile-time.

Basic Polymorphism: A Review

polymorphism, virtual, virtual functions

The chapter began with a list of five programming elements necessary 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 a program's behavior are expected skills. 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 the 5th requirement.

Simple Polymorphism And Non-Member Functions

polymorphism, virtual, Actor 5, virtual 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 them polymorphic.

virtual void display()
{
    cout << "Person:  " << name << endl;
    addr.display();		// composition
    if (date != nullptr)	// display if available
        date->display();	// aggregation
}
Actor 5 class diagram. Star is a subclass of Actor, and Actor is a subclass of Person. Person has an Address by composition, and has a Date by aggregation.
Person display: Enabling polymorphism. Making the display function virtual satisfies requirement 5, activating polymorphism. Adding the keyword is a deceptively simple step, yet it introduces complex structures and operations into a compiled program. Fortunately, polymorphism hides this complexity, allowing programmers to use it without concern about how C++ implements it. 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.

The following figure illustrates a program creating an instance of Star and calling its display function. The display functions are chained, so the calling sequence runs from the bottom of the inheritance hierarchy upwards to Person, which calls the display functions of its parts.

 

#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;
Person:  John Wayne
Address: California, 123 Palm Springs
Date:    1960/12/25
Actor:   Cranston Snort
Star:    5e+07
Person:  John Wayne
Address: California, 123 Palm Springs
Date:    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 to demonstrate 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 or polymorphic. Actor 5 adds labels to the output statements, correlating the output to a specific class.
  1. Making the display function virtual satisfies the five polymorphism requirements. So, despite s2 being a Person pointer, the statement s-⁠>display(); runs the Star display function, which is chained upwards to the overridden Actor and Person functions.
  2. Conversely, the statement cout << *s2 << endl; only runs the Person inserter, which is chained to the Address and Date classes.
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.

A Polymorphism Example with CList

polymorphism, CList, linked list, getter functions, Actor 5

The second, extended Actor 5 presents polymorphism in a slightly more authentic setting by storing objects in a dynamic data structure called a linked list. Linked Lists consist of nodes containing the data and one or more pointers called links that bind the nodes together. Lists are among the most flexible structures with numerous implementations and behaviors. Actor 5 uses a circularly linked list, formed like a necklace, with each node pointing or linked to the next 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 using the new keyword. 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 serves 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 with no data nodes, avoiding the boundary conditions encountered when inserting the first or removing the last data node.

The second Actor 5 example implements the circularly linked list named CList. Although library-grade data structures can generally store any data type, for simplicity, CList is limited to storing instances of Person and its subclasses. There are many varieties of linked lists, but, to demonstrate polymorphism, CList organizes the data alphabetically by name, implying that the storage order is not necessarily the same as the insertion order. The class's details are better suited to a data structures and algorithms course. Fortunately, we can treat it as a "black box," ignoring its implementation details and focusing on its public interface.

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, stores p in it, links it to the list in alphabetical order, and returns 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. A more general list class would rely on an overloaded operator<< in the data class or return the stored data to the client.
CList public interface. The CList member functions completely describe its public interface and the operations it supports. 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.

 

string getName()
{
	return name;
}
getName: A Person getter function. To maintain the list in alphabetical order, and to locate objects storing information for specific individuals, three CList functions, insert, search, and remove, require access to the name stored in each Person object. Consequently, Actor 5 adds a getter function, getName, to the Person class. A more general data structure that stores orderable object data typically requires it to overload operator< or operator().

To complete the polymorphism example, Actor 5 creates a CList and adds three different kinds of Person objects to it. The insert function maintains the objects in alphabetical order, so when the example prints them with the list function, the output order may differ from the insertion order. Nevertheless, polymorphism correctly calls the display method 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;
}
Actor 5 driver: Polymorphism and CList.
  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 Actor 5 Code

polymorphism, Actor 5, Address.h, Date.h, Person.h, Actor.h, Star.h, Actor5.cpp, CList.h, CList.cpp, Actor5_poly.cpp

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 The CList class specification
CList.cpp CList.cpp CList class member functions
Actor5_poly.cpp Actor5_poly.cpp The final Actor 5 example driver