1. Homepage of Dr. Zoltán Porkoláb
    1. Home
    2. Archive
  2. Teaching
    1. Timetable
    2. Bolyai College
    3. C++ (for mathematicians)
    4. Imperative programming (BSc)
    5. Multiparadigm programming (MSc)
    6. Programming (C in English)
    7. Programming languages (PhD)
    8. Software technology lab
    9. Theses proposals (BSc and MSc)
  3. Research
    1. CodeChecker
    2. CodeCompass
    3. Templight
    4. Projects
    5. Publications
    6. PhD students
  4. Affiliations
    1. Dept. of Programming Languages and Compilers
    2. Ericsson Hungary Ltd

21. Polymorphysm

Video: cpp-mat-ea21.mkv

Static and dynamic type

In the Inheritance lecture we have seen, that pointers and references of Base can refer to derived objects, but we can access only the Base part of the Derived objects thru them. The static type of the expressions using Base pointers and references is Base.

However, we frequently need to access the real, Derived type of the referred objects. That “real” type is called dynamic type.

Let see and example.

We want to store the vehicles: cars, trucks, buses in a list. As Vehicle is the common base type, first we try to store the elements of a list.

1
2
3
4
5
6
7
8
9
10
11
12
13
void f()
{
  std::list<Vehicle> vlst;   // base objects, won't work

  vlst.push_back( Car( ... ) );
  vlst.push_back( Truck( ... ) );
  vlst.push_back( Bus( ...) );

  for ( auto it = vlst.begin() it != vlst.end(); ++it)
  {
    std::cout << it->print() << '\n';
  }
}  

But the result is not what we expected:

[vehicle ... ]
[vehicle ... ]
[vehicle ... ]

The reason is that every derived object were sliced when inserted into the list. After that, we lost all information of the derived parts. Among other factors, every element of the list has room only for a Vehicle, no space for the additional attributes.

If we want to use such a mixed, polymorphic list, we have to allocate all objects on the heap, and we have to store the pointers to them. Of course, the pointers will have type of Base* as this is the common base. Do not forget to delete the objects on heap, otherwise we cause memory leak.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void f()
{
  std::list<Vehicle*> vlst;   // pointer to base

  vlst.push_back( new Car( ... ) );   // create on heap
  vlst.push_back( new Truck( ... ) );
  vlst.push_back( new Bus( ...) );

  for ( auto it = vlst.begin() it != vlst.end(); ++it)
  {
    std::cout << (*it)->print() << '\n'; // *it is pointer
  }
  // delete the objects to avoid memory leak
  for ( auto it = vlst.begin() it != vlst.end(); ++it)
  {
    delete (*it);   // won't work either
  }
}  

But the result is still not what we expected:

[vehicle ... ]
[vehicle ... ]
[vehicle ... ]

There are two problems:

Even, if a pointer ptr in the list is pointing to a derived object, the type of the pointer is Vehicle*, so the compiler thinks that *ptr is also Vehicle. The static type of *ptr is Vehicle. Therefore, (*ptr)->print will call Vehicle::print().

There is an other hidden problem too with the code above. The destructor which is called at delete (*it) is also Vehicle::~Vehicle() for similar reasons. If the destructors of the derived classes do some non trivial actions, we will miss those actions. This is perhaps more serious problem, than the previous one.

Virtual functions

We need a tool which makes us possible to reach the dynamic type of the objects even if we access them via pointers or references with static type of base.

This is what virtual functions are doing.

When we call a non-virtual function, the compiler decides what function is to execute based only on the static type. This decision happens at compilation time. However, when we call a virtual function the decision is happening in runtime based on the dynamic type of the actual object. This is why calling virtual functions is sometimes referred as late binding.

Of course, the compile still have to check in compilation time, whether the function call will be possible in runtime. As the compiler at compile time can know only the static type (typically the Base class), virtual functions have to be declared in the base class, and have to put an overriding function into the derived class(es).

Since the compiler when checks the existence of the function, the parameters, return type, etc. at compile time does not now which version (base or some derived) of the function will be called, all the overriding versions should be syntactically equivalent with the base virtual function. This means that all overriding versions should have exactly the same parameter list and (almost the same) return type. At the return type it is allowed, that if the base version returns withe some type X then the derived overriding version can return with some type X’ where X’ is a derived type of X. This relaxation is called as covariant return type.

Example

In our vehicle-car-bus-truck example we declare the base version of print() and mot() functions as virtual, thus it is possible to override them in the derived classes. We also declare the destructor as virtual, so if we delete an object via its base pointer we can execute the derived logic. It is always a good idea to declare the destructor as virtual if we have other virtual functions or we plan to access the derived objects via base pointers or references.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// vehicle.h
#ifndef VEHICLE_H
#define VEHICLE_H

#include <string>
#include "owner.h"  // for Owner type

class Vehicle
{
public:
  Vehicle( std::string pl, int hpw, Owner own); // constr
  virtual ~Vehicle() {}  // to ensure that derived 
                         // destructors will be called 
                         // (if exist)
  virtual bool mot() const;    // the test 
  virtual std::string print() const {
                          return std::string("[vehicle ") 
                              + plate() + /* ... */ ;   }
  std::string plate() const { _plate; }

  // Further Vehicle interface ...
private:
  std::string _plate;  // licence plate
  int         _hpw;    // horsepower
  Owner       _owner;  // owner of car  
  // Further Vehicle attributes ...
};
#endif // VEHICLE_H

In the derived classes we declare the overriding versions of the virtual functions. We use the override keyword to emphasize the fact that these are the overriding functions. Also this causes a syntax error when we make a mistake and the base class has no according function.

We do not declare here the destructor, as for this particular class the compiler will generate it automatically.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#ifndef CAR_H
#define CAR_H

#include <string>
#include "vehicle.h"

class Car : public Vehicle
{
public:
  Car( std::string pl, int hpw, Owner own, double emis) 
            : Vehicle( pl, hpw, own), _emis(emis) {}
  double emission () const { return _emis; }
  // the test for Car
  virtual bool mot() const override { return Vehicle::mot() 
                                        && _emis < 130; }
  virtual std::string print() const override 
                            { return std::sting("[car ")
                              + Vehicle::print() + 
                              /* car specific */ ;     }
private:
  double _emis; // gram/km CO2 emission
};
#endif /* CAR_H */

Similarly for Bus and Truck classes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void f()
{
  std::list<Vehicle*> vlst;   // pointer to base

  vlst.push_back( new Car( ... ) );   // create on heap
  vlst.push_back( new Truck( ... ) );
  vlst.push_back( new Bus( ...) );

  for ( auto it = vlst.begin() it != vlst.end(); ++it)
  {
    // will call the overriding version from derived
    std::cout << (*it)->print() << '\n'; 
  }
  // delete the objects to avoid memory leak
  for ( auto it = vlst.begin() it != vlst.end(); ++it)
  {
    delete (*it);   // will call derived destructor
  }
}  

Now the result is what we expected. The print() function called on the base pointers will call the overriding versions defined in the derived.

[car ...   ]
[truck ... ]
[bus ...   ]

Also, the correct destructors will be called.

Abstract classes

Sometimes we use the base class only to collect the common code of the derived classes. Like in our vehicle example, we have only Car, Bus and Truck objects but no Vehicle ones. Such class, like the Vehicle one which has no object, is called as abstract class. In C++ we express this declaring at least one of the virtual functions as pure virtual.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// vehicle.h
#ifndef VEHICLE_H
#define VEHICLE_H

#include <string>
#include "owner.h"  // for Owner type

class Vehicle   // abstract class -- no objects
{
public:
  Vehicle( std::string pl, int hpw, Owner own); 
  virtual ~Vehicle() {} 
  virtual bool mot() const = 0;         // pure virtual 
  virtual std::string print() const = 0 // pure virtual
  {
    return std::string("[vehicle ") + plate() + ...;
  }
  std::string plate() const { _plate; }

  // Further Vehicle interface ...
private:
  std::string _plate;  // licence plate
  int         _hpw;    // horsepower
  Owner       _owner;  // owner of car  
  // Further Vehicle attributes ...
};
#endif // VEHICLE_H

The class Vehicle is abstract - no objects of Vehicle can be created. This is true for the derived classes of Vehicle too, until they do not implement a non-pure overriding version of the pure function of base.

As we can see, we still can write a body of a pure virtual, however this is not necessary. We can call the body of a pure virtual explicitly using the scope operator: Vehicle::print().


Financed from the financial support ELTE won from the Higher Education Restructuring Fund of the Hungarian Government.