C++. Polymorphism. Virtual functions. General concepts

Polymorphism. Virtual functions. General concepts. Specifiers virtual and override. Examples


Contents


Search other resources:

1. Polymorphism. General concepts. Early and late binding. Calling a virtual function in a class hierarchy

Virtual functions implement the so-called polymorphism. The term “polymorphism” comes from the Greek words poly (many) and morphos (form). Polymorphism is the property of program code to change its behavior depending on the situation that arises during the execution of the program. In the implementation context, polymorphism is a technique for calling virtual functions implemented in hierarchically related classes. The class hierarchy is formed on the basis of the inheritance mechanism.

Closely related to the concept of polymorphism is the concept of a virtual function. This is a specially designed function that can be in the so-called polymorphic state – a state in which a call to the desired function from a set of virtual ones is formed at the late binding stage. The concept of late binding means that the code for calling the desired function is generated when the program is executed. In other words, in the source code, a function call is only denoted without specifying exactly which function should be called. The object on which the virtual function is called has a general meaning. A specific object and its corresponding function will be formed at the stage of program execution.

The virtual function mechanism implements the fundamental principle of polymorphism: “one interface, multiple implementations” or “one interface, multiple methods”.

As you know, there is also early binding. With early binding, you know which objects are used when calling the function in each case. This imposes restrictions on the capabilities of the program code. Changing objects for functions with the same name is not possible during program execution, this change must be programmed manually each time (this is the limitation of the code).

Not all tasks need to use late binding. The choice of which type of binding will be used in the program depends on the specifics of the problem being solved.

To implement late binding, the following is required:

  • classes are required to form a hierarchy using the inheritance mechanism;
  • functions have been defined in the class hierarchy that have the same name and list of parameters;
  • functions with the same name and parameters must be marked as virtual (with the virtual keyword).

Regarding the paradigm of classes and objects for polymorphism, the following characteristic features can be distinguished:

  • polymorphism does not characterize the object;
  • the implementation of polymorphism is determined by the architecture of the class;
  • polymorphism is a characteristic of member functions of a class;
  • the whole class cannot be polymorphic, only member functions of the class can be polymorphic.

 

2. Types of polymorphism. Dynamic polymorphism. Virtual function. Organization of a chain of virtual functions. Specifiers virtual and override

There are two types of polymorphism in C++:

  • static. This kind of polymorphism is achieved by using overloaded functions (early binding), class templates and operator overloading. You can read more about function overloading here, class templates here, and operator overloading here;
  • dynamic. In this case, inheritance is used in combination with virtual functions (late binding).

A virtual function is a function that is declared in a base class and overridden in a derived class. A derived class implements a virtual function at its own discretion. To declare a virtual function, the virtual keyword is used. The signature of a virtual function, declared in a base class, determines the kind of interface that the function implements. An interface defines how a virtual function is called. For each specific class, a virtual function has its own implementation, which provides the execution of actions specific only to this class. Thus, a virtual function for a specific class is a certain unique (specific) method (specific method).

In its most simplified form, the declaration of a virtual function in a class can be as follows:

class BaseClass
{
  virtual return_type FuncNameVirtual(list_of_parameters)
  {
    // ...
  }
};

here

  • FuncNameVirtual() – the name of the virtual function;
  • return_type – the type returned by the function;
  • list_of_parameters – the list of parameters that the function receives.

In the inherited class, the virtual function FuncNameVirtual() continues the chain of virtual functions for lower-level classes. You don’t need to specify the virtual keyword for this, because that word is already specified in the base class BaseClass.

class DerivedClass
{
  return_type FuncNameVirtual(list_of_parameters)
  {
    // It is also a virtual function that overrides the base class function.
    // ...
  }
}

There are situations when a function is declared in a derived class, which can be perceived as virtual, but it is not virtual. Examples of such situations:

  • a function with the same signature as in the base class, but declared as const;
  • a function to which arguments of a type compatible with the base class function arguments are passed.

In C++, in an inherited class, a virtual function that overrides a base class function of the same name can be declared with the override specifier. Although this specifier is not required, the declaration is needed for better informational content. A cursory examination of an inherited class immediately shows virtual functions (override). Based on this, an approximate form of an inherited class can be something like this

class DerivedClass
{
  return_type FuncNameVirtual(list_of_parameters) override
  {
    // For a chain of virtual functions, this should always be done
    // ...
  }
}

After specifying the override specifier, the programmer has better information about the fact that this function is virtual and it overrides the base class virtual function of the same name.

If a function declared with the override specifier in a DerivedClass derived class does not have a suitable virtual function in the base class BaseClass, the compiler will generate an error.

 

3. Cases of implementation of polymorphism

Calling a virtual function from client code is the same as calling a non-virtual function. The main thing here is the correct organization of the virtual function call.

If virtual functions are implemented in the class hierarchy, then polymorphism is implemented in the following cases:

  • when declaring a pointer (*) to the base class and calling the virtual function of the corresponding instance of the class that is part of the hierarchy. As you know, in this case the pointer to the base class can be set to the value of instances of derived classes. The corresponding virtual function is then called;
  • when passing a pointer (*) to the base class to some function that calls the virtual function of the base class using the -> operator (access by pointer);
  • when passing a reference (&) to a base class to a function that calls the virtual function of the base class using the ‘ . ‘ operator (dot, access by reference).

 

4. Examples of polymorphism implementation
4.1. An example of polymorphism for two classes. Calling a virtual function by pointer (->). Code analysis

The example demonstrates the mechanism of polymorphism, which consists in passing some function of a pointer to the base class. The base class and its subclass (a class inherited from the base class) contain a virtual PrintInfo() function with no parameters that prints an informational message about the given class.

#include <iostream>
using namespace std;

// Base class
class Base
{
public:
  // virtual - feature of a virtual function
  virtual void PrintInfo()
  {
    cout << "Base." << endl;
  }
};

// The class inherited from the Base class,
// important: there must be a public modifier here
class Derived : public Base
{
public:
  virtual void PrintInfo() override // it is desirable to specify the override specifier
  {
    cout << "Derived." << endl;
  }
};

void main()
{
  // 1. Create instances of base and derived class
  Base obj1;
  Derived obj2;

  // 2. Declare a pointer to a base class
  Base* p;

  // 3. Use the rule: a pointer to a base class can point to any instance
  //     of the base class and its derived class.
  //   Polymorphism is demonstrated below.
  // 3.1. Set the pointer p to an instance of the base class obj1 and call PrintInfo()
  p = &obj1;
  p->PrintInfo(); // Base

  // 3.2. Set the pointer p to an instance of the derived class and call PrintInfo()
  p = &obj2;
  p->PrintInfo(); // Derived - this is polymorphism (the word virtual)
}

The result of the program

Base.
Derived.

Let’s analyze the above code.

The example declares two classes, Base and Derived, which form a hierarchy using the inheritance mechanism.

To ensure polymorphism, the rule is used: for classes that form a hierarchy, a pointer to the base class can refer to an instance of the base class and any inherited class from this hierarchy. Therefore, the program declares the line

...

// 3. Declare pointer to the base class
Base* p;

...

Now the pointer p can be assigned the address of any instance of the Base and Derived classes. First, the address of the obj1 instance of type Base is assigned and the PrintInfo() method is called

p = &obj1;
p->PrintInfo(); // Base

The output will be predictable – the word “Base”.

The p pointer is then set to the address of the obj2 instance of type Base and the PrintInfo() method is called.

p = &obj2;
p->PrintInfo();

The output will be “Derived”. That is, the PrintInfo() method of the derived class will be called, which is what we need. Calling this method provides the virtual keyword in the declaration of the PrintInfo() function of the Base class.

If we remove the virtual keyword in the Base class before declaring the PrintInfo() function, then in the following code

p = &obj2;
p->PrintInfo();

the PrintInfo() method of the Base class will be called, not the Derived class. This means that polymorphism will not be supported and the base class function will always be called. As a result, the program will display

Base.
Base.

Thus, the PrintInfo() function of the Derived class for the pointer p to the base class Base will not be available.

Conclusion. Polymorphism implements the “one interface, many implementations” rule. In our case, there is only one interface – this is the declaration and call of the PrintInfo() function

p->PrintInfo();

But depending on which object the pointer p points to, the appropriate PrintInfo() method will be called – and there are many implementations.

 

4.2. An example explaining polymorphism for three classes. Passing a pointer (*) to a base class to a function

An example of 3 classes CalcLength, CalcArea, CalcVolume is given, in which the virtual function Calc() returns the circumference, the area of the circle and the volume of the sphere, respectively.

To demonstrate, some ShowResult() function is created, into which a pointer to the base class is passed. The function calls the virtual function Calc() by pointer. The body of the ShowResult() function does not know which class instance will be passed to it. The instance will be generated at runtime.

#include <iostream>
using namespace std;

// Class containing a function for calculating the circumference of a circle
class CalcLength
{
public:
  // Virtual function
  virtual double Calc(double radius)
  {
    return 2 * 3.1415 * radius;
  }
};

// Class containing a function for calculating the area of a circle
class CalcArea : public CalcLength
{
public:
  // Virtual function
  double Calc(double radius) override
  {
    return 3.1415 * radius * radius;
  }
};

// Class containing a function for calculating the volume of a sphere
class CalcVolume : public CalcArea
{
public:
  // Virtual function
  double Calc(double radius) override
  {
    return 4.0 / 3 * 3.1415 * radius * radius * radius;
  }
};

// Some function that receives a pointer to the base class ClassLength and a radius parameter,
// this function demonstrates polymorphism
void ShowResult(CalcLength* p, double radius)
{
  // Calling the Calc() method at the p pointer.
  // For this function, it is not known which class method will be called.
  // The desired method will be generated at runtime - this is polymorphism
  double res = p->Calc(radius); // <=== common interface for different implementations
  cout << "Result = " << res << endl;
}

void main()
{
  // 1. Declare a pointer to the base class - this is important
  CalcLength* p = nullptr;

  // 2. Create instances of 3 classes
  CalcLength obj1;
  CalcArea obj2;
  CalcVolume obj3;

  // 3. Enter function number
  int num;
  cout << "Enter number of function (1-3): ";
  cin >> num;

  if ((num < 1) || (num > 3))
    return;

  // 4. Input radius
  double radius;
  cout << "radius = ";
  cin >> radius;

  // 5. Set pointer p depending on input num
  if (num == 1) p = &obj1;
  if (num == 2) p = &obj2;
  if (num == 3) p = &obj3;

  // 6. Call the ShowResult() method
  //    The desired object is substituted depending on the situation.
  ShowResult(p, radius);

  // 7. Call the ShowResult() method directly substituting an instance of the class
  if (num == 1) ShowResult(&obj1, radius);
  if (num == 2) ShowResult(&obj2, radius);
  if (num == 3) ShowResult(&obj3, radius);
}

 

4.3. An example of polymorphism for three classes. Calling a virtual function in a method. Passing a reference (&) to a base class into a method

The example demonstrates polymorphism based on the base class reference passed to the DemoPolymorphism() function. 3 classes are declared with the names A, B, C. Class A is the base class for class B. Class B is the base class for class C. Classes contain only one method that displays the name of the class. Using the polymorphism mechanism, a reference to one of the instances is passed to the DemoPolymorphism() function. In accordance with the passed instance, the required method is called.

#include <iostream>
using namespace std;

// Class A - base class in hierarchy A <= B <= C
class A
{
public:
  virtual void Show()
  {
    cout << "A::Show()" << endl;
  }
};

// Class B - inherited from class A
class B : public A
{
public:
  // virtual method - overrides the same name method of class A
  void Show() override
  {
    cout << "B::Show()" << endl;
  }
};

// Class C - inherited from class B
class C : public B
{
public:
  // virtual method - overrides the same name method of class B
  void Show() override
  {
    cout << "C::Show()" << endl;
  }
};

// A function that receives a reference to the base class A,
// the function demonstrates polymorphism.
void DemoPolymorphism(A& ref)
{
  // Calling a virtual method, the method is determined on execution.
  // Here it is not known which method of class A, B or C should be called.
  ref.Show();
}

void main()
{
  // 1. Declare instances of classes A, B, C
  A objA;
  B objB;
  C objC;

  // 2. Call the DemoPolymorphism() function,
  //    depending on which instance is being passed,
  //    the corresponding class method is called.
  DemoPolymorphism(objA); // A::Show()
  DemoPolymorphism(objB); // B::Show()
  DemoPolymorphism(objC); // C::Show()
}

When run, the program will produce the following result

A::Show()
B::Show()
C::Show()

If the virtual and override specifiers are removed when declaring the Show() method in classes A, B, C, then polymorphism will not be supported. This means that when calling the DemoPolymorphism() method, all passed references to objA, objB, objC instances will be converted into a reference to the base class A&. As a result, the Show() method of class A will be called 3 times, and the program will return the following result

A::Show()
A::Show()
A::Show()

 


Related topics