|
Polymorphism |
|
What is Polymorphism? |
In programming languages, polymorphism means that some code or operations or objects behave differently in different contexts.
For example, the
+(plus) operator in C++:4 + 5 <-- integer addition 3.14 + 2.0 <-- floating point addition s1 + "bar" <-- string concatenation!In C++, that type of polymorphism is called overloading.
Typically, when the term polymorphism is used with C++, however, it refers to using
virtualmethods.
|
C++ Polymorphism Syntax |
|
Employee Example |
Here, we will represent 2 types of employees as classes in C++:
a generic employee (class
Employee)a manager (class
Manager)For these employees, we'll store data, like their:
name
pay rate
And...we'll require some functionality, like being able to:
initialize the employee
get the employee's fields
calculate the employee's pay
To help demonstrate polymorphism in C++, we'll focus on the methods that calculate an employee's pay.
|
|
Here is a class for a
generic Employee:
|
Note that the
payRate is used as an hourly
wage.
|
|
We'll also have a
Managerclass that is defined reusing theEmployeeclass (i.e., via inheritance).Remember, if a manager inherits from an employee, then it will get all the data and functionality of an employee. We can then add any new data and methods needed for a manager and override (i.e., redefine) any methods that differ for a manager.
// Definitions for the additional or overridden methods follow:
|
The
pay() method is given a new definition, in
which the payRate
has 2 possible uses. If the manager is salaried,
payRate is the fixed rate
for the pay period; otherwise, it represents an hourly rate, just like it does
for a regular employee.
|
Often, we want a derived class that is a "kind of" the base class:
Employee <-- generic employee | Manager <-- specific kind of employee, but still an "employee"In these cases,
publicinheritance:class Manager : public Employee {is the kind of inheritance that should be used.
I.e., if a
Manageris truly a "kind of"Employee, then it should have all the things (i.e., the same interface) that anEmployeehas.Deriving a class
publicly guarantees this, as all thepublicdata and methods from the base class remainpublicin the derived class.
|
Pointer to a base class |
A base class pointer can point to either an object of the base class or of any
publicly-derived class:Employee *emplP; if (condition1) { emplP = new Employee(...); } else if (condition2) { emplP = new Manager(...); }This allows us, for example, to write one set of code to deal with any kind of employee:
cout << "Name: " << emplP->getName(); cout << "Pay rate: " << emplP->getPayRate();Note: Typically, one just needs to write different code only to assign the pointer to the right kind of object, but not to call methods (as above).
|
Calling methods with base class pointers |
As you may suspect, calling
getName()orgetPayRate()using anEmployeepointer:cout << "Name: " << emplP->getName(); cout << "Pay rate: " << emplP->getPayRate();will do the same things (return the
namefield orpayRatefield) whether the pointer points to anEmployeeorManager.That's because both classes use the exact same version of those methods--the one defined in
Employee.
|
*** Overridden methods with base class pointers *** |
What, however, will happen when a method that was overridden is called?
Employee *emplP; if (condition1) { emplP = new Employee(...); } else if (condition2) { emplP = new Manager(...); } cout << "Pay: " << emplP->pay(40.0);Your first thought may be that the same thing that would happen with actual
EmployeeandManagerobjects would happen:Employee empl; Manager mgr; cout << "Pay: " << empl.pay(40.0); // calls Employee::pay() cout << "Pay: " << mgr.pay(40.0); // calls Manager::pay()In fact, that is not the case:
Employee *emplP; emplP = &empl; // make point to an Employee cout << "Pay: " << emplP->pay(40.0); // calls Employee::pay() emplP = &mgr; // make point to a Manager cout << "Pay: " << emplP->pay(40.0); // calls Employee::pay()By default, it is the type of the pointer (i.e.,
Employee), not the type of the object it points to (i.e., possiblyManager) that determines which version will be called:Employee *emplP; if (condition1) { emplP = new Employee(...); } else if (condition2) { emplP = new Manager(...); } cout << "Pay: " << emplP->pay(40.0); // calls Employee::pay()
|
|
We'd prefer that it call the version of
pay()that corresponds to the type of the object pointed to:Employee *emplP; emplP = &empl; // make point to an Employee cout << "Pay: " << emplP->pay(40.0); // call Employee::pay() emplP = &mgr; // make point to a Manager cout << "Pay: " << emplP->pay(40.0); // please--Manager::pay()?We can get that behavior by making the
pay()methodvirtual! We do so in its declaration:class Employee { public: ... virtual float pay(float hoursWorked) const; ... };
|
Non- |
The same behavior that
virtualmethods exhibit with pointers to objects extends to references.For example, suppose we wanted a function to print the pay for any kind of employee. If the
pay()method was notvirtual, we'd have to do something like:typedef enum {EMPL_PLAIN, EMPL_MANAGER} KindOfEmployee; void PrintPay(const Employee &empl, KindOfEmployee kind,float hoursWorked) { float amount; switch (kind) { case EMPL_PLAIN: amount = empl.pay(hoursWorked); break; case EMPL_MANAGER: // convert to Manager... const Manager &mgr = static_cast<const Manager &>(empl); // ...then call its pay() amount = mgr.pay(hoursWorked); break; } cout << "Pay: " << amount << endl; }Then no need to use virtual ? Every time a new type of employee is added, we must add another case with a nasty cast (and another value in the enumeration) .
|
|
If the
pay()method is declaredvirtual, the function can be written much simpler:void PrintPay(const Employee &empl,float hoursWorked) { cout << "Pay: " << empl.pay(hoursWorked) << endl; }If a new class that inherits (
publicly) fromEmployeeis later added, thenPrintPay()works for it too (without modification). FunctionPrintPay()can be used as:Employee empl; Manager mgr; PrintPay(empl, 40.0); PrintPay(mgr, 40.0);
|
Calling |
The polymorphic behavior of
virtualmethods extends to calling them within another method.For example, suppose the
pay()method has been declaredvirtualinEmployee.And, we add a
printPay()method to theEmployeeclass:void Employee::printPay(float hoursWorked) const { cout << "Pay: " << pay(hoursWorked) << endl; }which gets inherited in
Managerwithout being overridden.Which version of
pay()will be called withinprintPay()for aManager?Manager mgr; mgr.printPay(40.0);The
Managerversion ofpay()gets called inside ofprintPay()even thoughprintPay()was only defined inEmployee!
Why? Remember that:
void Employee::printPay(float hoursWorked) const { ... pay(hoursWorked) ... }is really shorthand for:
void Employee::printPay(float hoursWorked) const { ... this->pay(hoursWorked) ... }and we know that
virtualfunctions behave polymorphically with pointers!
|
Design Issues |
We can often write better code using polymorphism, i.e., using
publicinheritance, base class pointers (or references), andvirtualfunctions.
For example, we were able to write generic code to print any employee's pay:
void PrintPay(const Employee &empl,float hoursWorked) { cout << "Pay: " << empl.pay(hoursWorked) << endl; }That makes sense, since pay is printed the same for all employees.
The differences are only in how pay is calculated, and we were able to isolate those where they belong, in the different classes:
virtual float Employee::pay(float hoursWorked) const; virtual float Manager::pay(float hoursWorked) const;Nonetheless, using polymorphism to produce good designs takes thought.
For example, suppose we add a new kind of employee, a
Supervisor, with one of the following two choices of where to place the new class in the hierarchy:a) Employee b) Employee | / \ Manager Manager Supervisor | SupervisorIf we later write a function that takes a
Managerreference:void GiveRaise(Manager &mgr);Which class hierarchy would allow us to pass a
SupervisortoGiveRaise()?