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++:
In C++, that type of polymorphism is called overloading.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 usingvirtualmethods, 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
Employeeclass:Here is a class definition for a genericEmployee:
Definitions for each of the methods follow:class Employee { public: Employee(string theName, float thePayRate); string getName() const; float getPayRate() const; float pay(float hoursWorked) const; protected: string name; float payRate; };
Note that theEmployee::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; }payRateis used as an hourly wage.
Managerclass:We'll also have aManagerclass 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.
Here is the class definition for aManager:
Definitions for the additional or overridden methods follow:#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; };
TheManager::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 thepayRatehas 2 possible uses. If the manager is salaried,payRateis 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
EmployeeandManagerobjectsTheseEmployeeandManagerclasses can be used as follows:
Recall that a#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;Managerhas all the methods inherited fromEmployee, likegetName(), new versions for those it overrode, likepay(), plus ones it added, likegetSalaried().
- Why
publicinheritance:Often, we want a derived class that is a "kind of" the base class:
In these cases,Employee <-- generic employee | Manager <-- specific kind of employee, but still an "employee"publicinheritance:
is the kind of inheritance that should be used.I.e, if aclass Manager : public Employee {Manageris truly a "kind of"Employee, then it should have all the things (i.e., the same interface) that anEmployeehas.
Deriving a classpublicly guarantees this, as all thepublicdata and methods from the base class remainpublicin the derived class.
Note: Everything that wasprotectedin the base class remainsprotectedin the derived class. And, those things that wereprivatein the base class are not directly accessible in the derived class.
There is alsoprivateandprotectedinheritance, but they do not imply the same kind of reuse aspublicinheritance. Withprivateandprotectedinheritance, we cannot say that the derived class is a "kind of" the base class, since the interface the base class guarantees (i.e., itspublicparts) becomesprivateorprotected, respectively. Thus,privateandprotectedinheritance represent a different way of reusing a class.
As we'll see,publicinheritance 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
publicly-derived class:
This allows us, for example, to write one set of code to deal with any kind of employee: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 anEmployeepointer:
will do the same things (return thecout << "Name: " << emplP->getName(); cout << "Pay rate: " << emplP->getPayRate();
namefield orpayRatefield) whether the pointer points to anEmployeeorManager.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?
Your first thought may be that the same thing that would happen with actualEmployee *emplP; if (condition1) { emplP = new Employee(...); } else if (condition2) { emplP = new Manager(...); } cout << "Pay: " << emplP->pay(40.0);EmployeeandManagerobjects would happen:
In fact, that is not the case:Employee empl; Manager mgr; cout << "Pay: " << empl.pay(40.0); // calls Employee::pay() cout << "Pay: " << mgr.pay(40.0); // calls Manager::pay()
By default, it is the type of the pointer (i.e.,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:
We'd prefer that it call the version ofEmployee *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:
We can get that behavior by making theEmployee *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 isvirtualin all derived classes too. We prefer to explicitly label it asvirtualin derived classes as a reminder:class Manager : public Employee { public: ... virtual float pay(float hoursWorked) const; ... };
virtualmethods with references:The same behavior thatvirtualmethods 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:
Every time a new type of employee is added, we must add another case with a nasty cast (and another value in the enumeration).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:
If a new class that inherits (void PrintPay(const Employee &empl, float hoursWorked) { cout << "Pay: " << empl.pay(hoursWorked) << endl; }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
virtualmethods within other methods:The polymorphic behavior ofvirtualmethods extends to calling them within another method.
For example, suppose thepay()method has been declaredvirtualinEmployee.
And, we add aprintPay()method to theEmployeeclass:
which gets inherited invoid Employee::printPay(float hoursWorked) const { cout << "Pay: " << pay(hoursWorked) << endl; }Managerwithout being overridden.
Which version ofpay()will be called withinprintPay()for aManager?
TheManager mgr; mgr.printPay(40.0);
Managerversion ofpay()gets called inside ofprintPay()even thoughprintPay()was only defined inEmployee!
Why? Remember that:
is really shorthand for:void Employee::printPay(float hoursWorked) const { ... pay(hoursWorked) ... }
and we know thatvoid Employee::printPay(float hoursWorked) const { ... this->pay(hoursWorked) ... }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:
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:void PrintPay(const Employee &empl, float hoursWorked) { cout << "Pay: " << empl.pay(hoursWorked) << endl; }
Nonetheless, using polymorphism to produce good designs takes thought.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:
If we later write a function that takes aa) Employee b) Employee | / \ Manager Manager Supervisor | SupervisorManagerreference:
Which class hierarchy would allow us to pass avoid GiveRaise(Manager &mgr);
SupervisortoGiveRaise()?
- Exercise:Take the code we've provided for the
Employeeclass (employee.h and employee.cpp) and theManagerclass (manager.h and manager.cpp).
- Add a method
print()to theEmployeeclass 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
Managerto 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 thevirtualmechanism).
virtualif it is necessary to make the above work. - Add a method
No comments:
Post a Comment