Previous Section
 < Free Open Study > 
Next Section


6.6 Polymorphism with Virtual Functions

In addition to encapsulation and inheritance, the third capability that must be available in an object-oriented programming language is polymorphism. In Chapter 2, we defined polymorphism as the ability to determine which function to apply to a particular object. This determination can be made at compile time (static binding) or at run time (dynamic binding). For a language to be truly object-oriented, it must support both static and dynamic binding; that is, it must support polymorphism. C++ uses virtual functions to implement run-time binding.

The basic C++ rule for passing parameters is that the actual parameter and its corresponding formal parameter must be of an identical type. With inheritance, C++ relaxes this rule somewhat. The type of the actual parameter may be an object of a derived class of the formal parameter.[1] To force the compiler to generate code that guarantees dynamic binding of a member function to a class object, the reserved word virtual appears before the function declaration in the declaration of the base class. Virtual functions work in the following way. If a class object is passed by reference to some function, and if the body of that function contains a statement

formalParameter.MemberFunction(...);

then

  1. If MemberFunction is not a virtual function, the type of the formal parameter determines which function to call. (Static binding is used.)

  2. If MemberFunction is a virtual function, the type of the actual parameter determines which function to call. (Dynamic binding is used.)

Let's look at an example. Suppose that ItemType is declared as follows:

class ItemType
{
public:
  
  virtual RelationType ComparedTo(ItemType) const;
private:
  char lastName[50];
  
};

RelationType ItemType::ComparedTo(ItemType item) const
{
  int result;

  result = std::strcmp(lastName, item.lastName);
  if (result < 0)
    return LESS;
  else if (result > 0)
    return GREATER;
  else return EQUAL;
}

Now let's derive a class NewItemType that contains two strings as data members. We want ComparedTo to use both of them in the comparison.

class NewItemType : public ItemType
{
Public:
  
  RelationType ComparedTo(ItemType) const;
Private:
  // In addition to the inherited lastName member
  char firstName[50];
  
};
RelationType NewItemType::ComparedTo(NewItemType item) const
{
  int result;

  result = std::strcmp(lastName, item.lastName);
  if (result < 0)
    return LESS;
  else if (result > 0)
    return GREATER;
  else
  {
    result = strcmp(firstName, item.firstName);
    if (result < 0)
      return LESS;
    else if (result > 0)
      return GREATER;
    else
      return EQUAL;
  }
}

The function ComparedTo is marked as virtual in the base class (ItemType); according to the C++ language, ComparedTo is therefore a virtual function in all derived classes as well. Whenever an object of type ItemType or NewItemType is passed by reference to a formal parameter of type ItemType, the determination of which ComparedTo to use within that function is postponed until run time. Let's assume that the client program includes the following function:

void PrintResult(ItemType& first, ItemType& second)
{
  using namespace std;
  if (first.ComparedTo(second)==LESS)
    cout  << "First comes before second";
  else
    cout  << "First does not come before second";
}

It then executes the following code:

ItemType item1. item2;
NewItemType item3, item4:
  
PrintResult(item1, item2);
PrintResult(item3, item4);

Because item3 and item4 are objects of a class derived from ItemType, both of the calls to PrintResult are valid. PrintResult invokes ComparedTo. Which one? Is it ItemType::ComparedTo or NewItemType::ComparedTo? Because ComparedTo is a virtual function and the class objects are passed by reference to PrintResult, the type of the actual parameter-not the formal parameter-determines which version of ComparedTo is called. In the first call to PrintResult, ItemType::ComparedTo is invoked; in the second call, NewItemType::ComparedTo is invoked. This situation is illustrated in the following diagram:

Click To expand

This example demonstrates an important benefit of dynamic binding. The client does not need to have multiple versions of the PrintResult function, one for each type of parameter that is passed to it. If new classes are derived from ItemType (or even from NewItemType), objects of those classes can be passed to PrintResult without any modification of PrintResult.

If you have a pointer defined as a pointer to a base class and dynamically allocate storage using the base type, the pointer points to a base-class object. If you dynamically allocate storage using the derived type, the pointer points to a derived-class object. Take, for example, the following short program with a base class One and a derived class Two. Here we allocate an object of (base) class One and an object of (derived) class Two.

#include <iostream>
class One
{
Public:
  virtual void Print() const:
};

class Two : public One
{
Public:
  void Print() const;
};

void PrintTest(One*);

int main()
{
  using namespace std;
  One* onePtr;
  onePtr = new One;

  cout << "Result of passing an object of class One: ";
  PrintTest(onePtr);

  onePtr = new Two;

  cout << "Result of passing an object of class Two: ";

  PrintTest(onePtr);
  return 0;
}
void PrintTest(One* ptr)
{
  ptr->Print();
}

void One::Print() const
{
  std::cout  << "Print member function of class One" << end1;
}

void Two::Print() const
{
  std::cout  << "Print member function of class Two " << end1;
}

onePtr points first to an object of class One and then to an object of class Two. When the parameter to PrintTest points to an object of class One, the class One member function is applied. When the parameter points to an object of class Two, the class Two member function is applied. The fact that the type of the run-time object determines which member function is executed is verified by the following output:

Click To expand

We must issue one word of caution about passing a parameter of a derived type to any function whose formal parameter is of the base type. If you pass the parameter by reference, no problem arises. If you pass the parameter by value, however, only the subobject that is of the base type is actually passed. For example, if the base type has two data members and the derived type has two additional data members, only the two data members of the base type are passed to a function if the formal parameter is of the base type and the actual parameter is of the derived type. This slicing problem (any additional data members declared by the derived class are "sliced off") can also occur if we assign an object of the derived type to an object of the base type.

Look back at Figure 4.14, which shows the relationship of objects of QueType and CountedQueType. If a CountedQueType object is passed as a value parameter to a function whose formal parameter is of type QueType, only those data members of QueType are copied; length, a member of CountedQueType, is not. Although this slicing does not present a problem in this case, be aware of this situation when designing your hierarchy of classes.

[1]This relaxation allows dynamic binding to occur.



Previous Section
 < Free Open Study > 
Next Section
Converted from CHM to HTML with chm2web Pro 2.85 (unicode)