Introduction to Polymorphism in C++
- Introduction:Modern object-oriented (OO) languages provide 3 capabilities:
- encapsulation
- inheritance
- polymorphism
Here, we'll explore how the programming capability known as polymorphism can be used in C++.
Note: You should already have some understanding of the first two concepts before attempting this material.
- 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!
Typically, when the term polymorphism is used with C++, however, it refers to usingvirtual
methods, which we'll discuss shortly.
- Employee example:Here, we will represent 2 types of employees as classes in C++:
- a generic employee (class
Employee
) - a manager (class
Manager
)
- name
- pay rate
- initialize the employee
- get the employee's fields
- calculate the employee's pay
- a generic employee (class
Employee
class:Here is a class definition for a genericEmployee
:
class Employee { public: Employee(string theName, float thePayRate); string getName() const; float getPayRate() const; float pay(float hoursWorked) const; protected: string name; float payRate; };
Employee::Employee(string theName, float thePayRate) { name = theName; payRate = thePayRate; } string Employee::getName() const { return name; } float Employee::getPayRate() const { return payRate; } float Employee::pay(float hoursWorked) const { return hoursWorked * payRate; }
payRate
is used as an hourly wage.
Manager
class:We'll also have aManager
class that is defined reusing theEmployee
class (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.
Here is the class definition for aManager
:
#include "employee.h" class Manager : public Employee { public: Manager(string theName, float thePayRate, bool isSalaried); bool getSalaried() const; float pay(float hoursWorked) const; protected: bool salaried; };
Manager::Manager(string theName, float thePayRate, bool isSalaried) : Employee(theName, thePayRate) { salaried = isSalaried; } bool Manager::getSalaried() const { return salaried; } float Manager::pay(float hoursWorked) const { if (salaried) return payRate; /* else */ return Employee::pay(hoursWorked); }
pay()
method is given a new definition, in which thepayRate
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.
Note: Employees paid by a salary (i.e., those that are salaried) get a fixed amount of money each pay period (e.g., week, 2 weeks, month) regardless of how many hours they work.
- Using
Employee
andManager
objectsTheseEmployee
andManager
classes can be used as follows:
#include "employee.h" #include "manager.h" ... // Print out name and pay (based on 40 hours work). Employee empl("John Burke", 25.0); cout << "Name: " << empl.getName() << endl; cout << "Pay: " << empl.pay(40.0) << endl; Manager mgr("Jan Kovacs", 1200.0, true); cout << "Name: " << mgr.getName() << endl; cout << "Pay: " << mgr.pay(40.0) << endl; cout << "Salaried: " << mgr.getSalaried() << endl;
Manager
has all the methods inherited fromEmployee
, likegetName()
, new versions for those it overrode, likepay()
, plus ones it added, likegetSalaried()
.
- Why
public
inheritance: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"
public
inheritance:class Manager : public Employee {
Manager
is truly a "kind of"Employee
, then it should have all the things (i.e., the same interface) that anEmployee
has.
Deriving a classpublic
ly guarantees this, as all thepublic
data and methods from the base class remainpublic
in the derived class.
Note: Everything that wasprotected
in the base class remainsprotected
in the derived class. And, those things that wereprivate
in the base class are not directly accessible in the derived class.
There is alsoprivate
andprotected
inheritance, but they do not imply the same kind of reuse aspublic
inheritance. Withprivate
andprotected
inheritance, we cannot say that the derived class is a "kind of" the base class, since the interface the base class guarantees (i.e., itspublic
parts) becomesprivate
orprotected
, respectively. Thus,private
andprotected
inheritance represent a different way of reusing a class.
As we'll see,public
inheritance makes writing generic code easier.
- Pointer to a base classA base class pointer can point to either an object of the base class or of any
public
ly-derived class:
Employee *emplP; if (condition1) { emplP = new Employee(...); } else if (condition2) { emplP = new Manager(...); }
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 anEmployee
pointer:
cout << "Name: " << emplP->getName(); cout << "Pay rate: " << emplP->getPayRate();
name
field orpayRate
field) whether the pointer points to anEmployee
orManager
.That's because both classes use the exact same version of those methods--the one defined inEmployee
.
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);
Employee
andManager
objects would happen:
Employee empl; Manager mgr; cout << "Pay: " << empl.pay(40.0); // calls Employee::pay() cout << "Pay: " << mgr.pay(40.0); // calls Manager::pay()
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()
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()
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()?
pay()
methodvirtual
! We do so in its declaration:class Employee { public: ... virtual float pay(float hoursWorked) const; ... };
Note: Once a method is declared asvirtual
, it isvirtual
in all derived classes too. We prefer to explicitly label it asvirtual
in derived classes as a reminder:class Manager : public Employee { public: ... virtual float pay(float hoursWorked) const; ... };
virtual
methods with references:The same behavior thatvirtual
methods 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 thepay()
method was notvirtual
, we'd have to do something like:
typedef enum {EMPL_PLAIN, EMPL_MANAGER} KindOfEmployee; void PrintPay0(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; }
FunctionPrintPay0()
can be used as:Employee empl; Manager mgr; PrintPay0(empl, EMPL_PLAIN, 40.0); PrintPay0(mgr, EMPL_MANAGER, 40.0);
If thepay()
method is declaredvirtual
, the function can be written much simpler:
void PrintPay(const Employee &empl, float hoursWorked) { cout << "Pay: " << empl.pay(hoursWorked) << endl; }
public
ly) fromEmployee
is 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
virtual
methods within other methods:The polymorphic behavior ofvirtual
methods extends to calling them within another method.
For example, suppose thepay()
method has been declaredvirtual
inEmployee
.
And, we add aprintPay()
method to theEmployee
class:
void Employee::printPay(float hoursWorked) const { cout << "Pay: " << pay(hoursWorked) << endl; }
Manager
without being overridden.
Which version ofpay()
will be called withinprintPay()
for aManager
?
Manager mgr; mgr.printPay(40.0);
Manager
version ofpay()
gets called inside ofprintPay()
even thoughprintPay()
was only defined inEmployee
!
Why? Remember that:
void Employee::printPay(float hoursWorked) const { ... pay(hoursWorked) ... }
void Employee::printPay(float hoursWorked) const { ... this->pay(hoursWorked) ... }
virtual
functions behave polymorphically with pointers! - Design issues:We can often write better code using polymorphism, i.e., using
public
inheritance, base class pointers (or references), andvirtual
functions.
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; }
virtual float Employee::pay(float hoursWorked) const; virtual float Manager::pay(float hoursWorked) const;
For example, suppose we add a new kind of employee, aSupervisor
, with one of the following two choices of where to place the new class in the hierarchy:
a) Employee b) Employee | / \ Manager Manager Supervisor | Supervisor
Manager
reference:
void GiveRaise(Manager &mgr);
Supervisor
toGiveRaise()
?
- Exercise:Take the code we've provided for the
Employee
class (employee.h and employee.cpp) and theManager
class (manager.h and manager.cpp).
- Add a method
print()
to theEmployee
class that prints out some employee data for a pay period. E.g.:Name: John Burke Pay rate: 25 Pay: 1000
Yourprint()
method should print out the name and pay rate itself, but it should call theprintPay()
method to print the pay.
Note:print()
will have to receive the number of hours worked.
Your code should compile and run correctly with the test programpolytest.cpp
.
- After getting the first part to work, consider that we want the pay for a
Manager
to be printed differently than for anEmployee
. I.e., print the manager's pay labelled as "Salary:
" if they are salaried, and as "Wages:
" if they are not.Do so by overriding (i.e., redefining) theprintPay()
method inManager
.
Make any other changes that are necessary for other methods, likeprint()
, to work correctly withprintPay()
(Hint: use thevirtual
mechanism).
virtual
if it is necessary to make the above work. - Add a method
No comments:
Post a Comment