web analytics

Understanding Polymorphism and Virtual Functions in C++

Options
@2017-09-27 22:53:04

Abstract Classes and Pure Virtual Functions

You can encounter situations where you want to have a class to use as a base class for a number of other classes, but you do not have any meaningful definition to give to one or more of its member functions. When we introduced virtual functions we discussed one such scenario. Let's review it now.

Suppose you are designing software for a graphics package that has classes for several kinds of figures, such as rectangles, circles, ovals, and so forth. Each figure might be an object of a different class, such as the Rectangle class or the Circle class. In a well-designed programming project, all of them would probably be descendents of a single parent class called, for example, Figure. Now, suppose you want a function to draw a figure on the screen. To draw a circle, you need different instructions from those you need to draw a rectangle. So, each class needs to have a different function to draw its kind of figure. If r is a Rectangle object and c is a Circle object, then r.draw( ) and c.draw( ) can be functions implemented with different code.

Now, the parent class Figure may have a function called center that moves a figure to the center of the screen by erasing it and then redrawing it in the center of the screen. The function Figure::center might use the function draw to redraw the figure in the center of the screen. By making the member function draw a virtual function, you can write the code for the member function Figure::center in the class Figure and know that when it is used for a derived class, say Circle, the definition of draw in the class Circle will be the definition used. You never plan to create an object of type Figure. You only intend to create objects of the derived classes such as Circle and Rectangle. So, the definition that you give to Figure::draw will never be used. However, based only on what we covered so far, you would still need to give a definition for Figure::draw, even though it could be trivial.

If you make the member function Figure::draw a pure virtual function, then you do not need to give any definition to that member function. The way you make a member function into a pure virtual function is to mark it virtual and to add the annotation = 0 to the member function declaration as in the following example:

virtual void draw( ) = 0;

Any kind of member can be made a pure virtual function. It need not be a void function with no parameters as in our example.

A class with one or more pure virtual functions is called an abstract class. An abstract class can only be used as a base class to derive other classes. You cannot create objects of an abstract class, since it is not a complete class definition. An abstract class is a partial class definition because it can contain other member functions that are not pure virtual functions. An abstract class is also a type, so you can write code with parameters of the abstract class type and it will apply to all objects of classes that are descendents of the abstract class.

If you derive a class from an abstract class it will itself be an abstract class unless you provide definitions for all the inherited pure virtual functions (and also do not introduce any new pure virtual functions). If you do provide definitions for all the inherited pure virtual functions (and also do not introduce any new pure virtual functions) the resulting class is a not an abstract class, which means you can create objects of the class.

Programming Example: An Abstract Class

Display 6 shows the interface for the class Employee.

Display 6—Interface for the Abstract Class Employee

//This is the header file employee.h. 
//This is the interface for the abstract class Employee.

#ifndef EMPLOYEE_H
#define EMPLOYEE_H

#include <string>
using std::string;

namespace SavitchEmployees
{

  class Employee
  {

  public:
    Employee( );
    Employee(string theName, string theSsn);
    string getName( ) const;
    string getSsn( ) const;
    double getNetPay( ) const;
    void setName(string newName); 
    void setSsn(string newSsn);
    void setNetPay(double newNetPay);
    virtual void printCheck( ) const = 0;
  private:
    string name; 
    string ssn; 
    double netPay;

  };
//The implementation for this class is the same as in Chapter 14, 
//except that no definition is given for the member function printCheck( )
}//SavitchEmployees

#endif //EMPLOYEE_H

The word virtual and the = 0 in the member function heading tell the compiler this is a pure virtual function and so the class Employee is now an abstract class. The implementation for the class Employee includes no definition for the class Employee::printCheck (but otherwise the implementation of the class Employee is the same as before).

It makes sense that there is no definition for the member function Employee::printCheck, since you do not know what kind of check to write until know what kind of employee you are dealing with.

@2017-09-27 23:05:04

Virtual Functions and Extended Type Compatibility

If Derived is a derived class of the base class Base, then you can assign an object of type Derived to a variable (or parameter) of type Base, but not the other way around. If you consider a concrete example, this becomes sensible. For example, DiscountSale is a derived class of Sale. (Refer to Displays 1 and 3.) You can assign an object of the class DiscountSale to a variable of type Sale, since a DiscountSale is a Sale. However, you cannot do the reverse assignment, since a Sale is not necessarily a DiscountSale. The fact that you can assign an object of a derived class to a variable (or parameter) of its base class is critically important for reusing of code via inheritance. However, it does have its problems.

For example, suppose a program or unit contains the following class definitions:

class Pet 
{
public:
  string name;
  virtual void print( ) const;
};

class Dog : public Pet
{
public:
  string breed;
  virtual void print( ) const;   //keyword virtual not needed,
               //but put here for clarity.
};

Dog vdog;
Pet vpet;

Now concentrate on the data members, name and breed. (To keep this example simple, we have made the member variables public. In a real application, they should be private and have functions to manipulate them.)

Anything that is a Dog is also a Pet. It would seem to make sense to allow programs to consider values of type Dog to also be values of type Pet and hence the following should be allowed:

vdog.name = "Tiny"; 
vdog.breed = "Great Dane";
vpet = vdog;

C++ does allow this sort of assignment. You may assign a value, such as the value of vdog to a variable of a parent type, such as vpet (but you are not allowed to perform the reverse assignment). Although the above assignment is allowed, the value that is assigned to the variable vpet loses its breed field. This is called the slicing problem. The following attempted access will produce an error message:

cout << vpet.breed; 
    // Illegal: class Pet has no member named breed

You can argue that this makes sense, since once a Dog is moved to a variable of type Pet it should be treated like any other Pet and not have properties peculiar to Dogs. This makes for a lively philosophical debate, but it usually just makes for a nuisance when programming. The dog named Tiny is still a Great Dane and we would like to refer to its breed, even if we treated it as a Pet someplace along the way.

Fortunately, C++ does offer us a way to treat a Dog as a Pet without throwing away the name of the breed. To do this, we use pointers to dynamic variables.

Suppose we add the following declarations:

Pet *ppet; 
Dog *pdog;

If we use pointers and dynamic variables we can treat Tiny as a Pet without losing his breed. The following is allowed.

pdog = new Dog;
pdog->name = "Tiny";
pdog->breed = "Great Dane";
ppet = pdog; 

Moreover, we can still access the breed field of the node pointed to by ppet. Suppose that

Dog::print( ) const;

has been defined as follows:

void Dog::print( ) const
{
  cout << "name: " << name << endl;
  cout << "breed: " << breed << endl; 
}

The statement

ppet->print( );

will cause the following to be printed on the screen:

name: Tiny 
breed: Great Dane

This nice output happens by virtue of the fact that print( ) is a virtual member function. (No pun intended.) We have included test code in Display 7.

Display 7—Defeating the Slicing Problem

//Program to illustrate use of a virtual function to defeat the slicing problem.
#include <string>
#include <iostream>
using std::string;
using std::cout;
using std::endl;

class Pet
{
public:
//We have made the member variables public to keep the example simple. In a 
//real application they should be private and accessed via member functions.
  string name;
  virtual void print( ) const;
};

class Dog : public Pet
{
public: 
  string breed;
  virtual void print( ) const;
};

int main( )
{
  Dog vdog;
  Pet vpet;

  vdog.name = "Tiny"; 
  vdog.breed = "Great Dane";
  vpet = vdog; 
  cout << "The slicing problem:
";
  //vpet.breed; is illegal since class Pet has no member named breed.
  vpet.print( );
  cout << "Note that it was print from Pet that was invoked.
";

  cout << "The slicing problem defeated:
";
  Pet *ppet; 
  ppet = new Pet;
  Dog *pdog;
  pdog = new Dog;
  pdog->name = "Tiny";
  pdog->breed = "Great Dane";
  ppet = pdog; 
  //These two print the same output:
  //name: Tiny 
  //breed: Great Dane
  ppet->print( ); 
  pdog->print( ); 

  //The following, which accesses member variables directly
  //rather than via virtual functions would produce an error:
  //cout << "name: " << ppet->name << " breed: " 
  //   << ppet->breed << endl;

  return 0;
}

void Dog::print( ) const
{
  cout << "name: " << name << endl;
  cout << "breed: " << breed << endl; 
}

void Pet::print( ) const
{
  cout << "name: " << name << endl;
}
Sample Dialogue
The slicing problem:
name: Tiny 
Note that it was print from Pet that was invoked.
The slicing problem defeated:
name: Tiny 
breed: Great Dane
name: Tiny 
breed: Great Dane

Object-oriented programming with dynamic variables is a very different way of viewing programming. This can all be bewildering at first. It will help if you keep two simple rules in mind:

  1. If the domain type of the pointer pAncestor is an ancestor class for the domain type of the pointer pDescendent, then the following assignment of pointers is allowed:

    pAncestor = pDescendent;

    Moreover, none of the data members or member functions of the dynamic variable being pointed to by pDescendent will be lost.

  2. Although all the extra fields of the dynamic variable are there, you will need virtual member functions to access them.

Pitfall: The Slicing Problem

Although it is legal to assign a derived class object into a base class variable, assigning a derived class object to a base class object slices off data. Any data members in the derived class object that are not also in the base class will be lost in the assignment, and any member functions that are not defined in the base class are similarly unavailable to the resulting base class object.

For example, if Dog is a derived class of Pet, then the following is legal:

Dog vdog;
Pet vpet;
vpet = vdog; 

However, vpet cannot be a calling object for a member function from Dog unless the function is also a member function of Pet, and all the member variables of vdog that are not inherited from the class Pet are lost. This is the slicing problem.

Note that simply making a member function virtual does not defeat the slicing problem. Note the following code from Display 7:

Dog vdog;
Pet vpet;

vdog.name = "Tiny"; 
vdog.breed = "Great Dane";
vpet = vdog; 
. . .
vpet.print( );

Although the object in vdog is of type Dog, when vdog is assigned to the variable vpet (of type Pet) it becomes an object of type Pet. So, vpet.print( ) invokes the version of print( ) defined in Pet, not the version defined in Dog. This happens despite the fact that print( ) is virtual. In order to defeat the slicing problem, the function must be virtual and you must use pointers and dynamic variables.

Comments

You must Sign In to comment on this topic.


© 2024 Digcode.com