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 (MSc Aut. Sys.)
    7. Programming languages (PhD)
    8. Software technology lab
    9. Theses proposals (BSc and MSc)
  3. Research
    1. Sustrainability
    2. CodeChecker
    3. CodeCompass
    4. Templight
    5. Projects
    6. Conferences
    7. Publications
    8. PhD students
  4. Affiliations
    1. Dept. of Programming Languages and Compilers
    2. Ericsson Hungary Ltd

20. Inheritance

Video: cpp-mat-ea20.mkv

In this chapter we discuss the inheritance in C++. We do not suppose that the reader knows the theory of inheritance, in this chapter we concentrate on the practical benefits and the C++ implementation of inhertitance.

Generalisation and specialization

Let suppose that we want to implement a software for British MOT-like test – an annual test of vehicle safety, road-worthiness aspects (see wikipedia). Naturally, our example is just a simplification of the real tests and serves only demonstration purposes.

One way to implement the system is to create a common class for all vehicles.

1
2
3
4
5
6
7
8
9
10
11
12
class Vehicles
{
public:
  // Contructor 
  Vehicle( std::string pl, int hpw, Owner own); 
  bool mot() const;           // the test itself
  std::string plate() const { _plate; }
  // Further Vehicle interface ...
private:
  std::string _plate;  // licence plate
  // Further Vehicle attributes ...
};

We start to collect the features of the different vehicles. We recognise soon that there are common attributes of all vehicles: _ plate_, horsepower and owner. However, there are also feautures typical only for a specific type of vehicle: CO emission for cars, length and number of max passanger persons for busses, and total weight and number of axles for trucks. The problem is that some of the attributes valid only for some kind of objects, e.g. axles are not interesting for buses. The major problem is, however, that for the MOT test some of the attributes are interesting only for some specific kind of objects, e.g. axles are not interesting for buses but for trucks only.

To fix that, we have to encounter all attributes into the same class - and let some of them unused when we have an other type of vehicle - or we have to use the union construct. In any cases, we have to store the kind of the vehicle.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Vehicles
{
public:
  enum kind_t { car, truck, bus };
  
  Vehicle( kind_t kind, std::string pl, int hpw, Owner own, 
                               double emission);   // Car
  Vehicle( kind_t kind, std::string pl, int hpw, Owner own, 
                               int persons);       // Bus
  Vehicle( kind_t kind, std::string pl, int hpw, Owner own, 
                               int w, int axl);    // Truck
  bool mot() const;    // the test itself
  // Further Vehicle interface ...
  std::string plate() const { _plate; }
private:
  kind_t      _kind;   // what kind of vehicle
  std::string _plate;  // licence plate
  int         _hpw;
  Owner       _owner;
  double      _emission;  // only for car
  int         _persons;   // only for bus
  int         _w;         // only for truck
  int         _axl;       // only for truck
};

One problem is that attributes not used in a specific vehicle will be unitialized or have some extremal value (like null pointer or similar). This is a serious source of mistakes.

The vehicles to test are cars, buses, and trucks and each kind of vehicle requires different test details.

1
2
3
4
5
6
7
8
9
10
// bad solution
bool Vehicle::mot() const
{
  if ( this object is a Car )
    // evaluate Car-specific tests
  else if ( this object is a Bus )
    // evaluate Bus-specific tests
  else if ( this object is a Truck )
    // evaluate Truck-specific tests
}

Although, there are techniques to identify the actual class of an object (we can use an enumerator value set in the constructor or the typeid operator), this solution is also a maintenance nightmare. Every time we add a new type of vehicle to the system, we have to find all the type selections in the code, modify and, and of course have to re-test everything.

Perhaps it would be better to mix the previous solutions: we create separate classes for cars, buses, trucks, but we still want to keep common attributes and methods in one separate, well-defined class.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Car
{
public:
  // Interface for cars
private:
  Vehicle _veh; // common attributes for all vehicles
  // Car specific parts
};
class Truck
{
public:
  // Interface for trucks
private:
  Vehicle _veh; // common attributes for all vehicles
  // Truck specific parts
};
// etc...

The problem is, that the Vehicle is not part of the Car/Truck interface, e.g. calling c.plate() on a c Car object is error, since the Vehicle functions does not exist automatically on Car, Bus, Truck classes. We should solve this manually:

1
2
3
4
5
6
7
8
9
10
class Car
{
public:
  // ...
  // Lots of code for forwarding Vehicle interface to Car
  std::string plate() { return _veh._plate; }
private:
  Vehicle _veh; // common attributes for all vehicles
  // Car specific parts
};

Inheritance

Inheritance provides the solution: while the derived class containes all attributes inherited from the base class (plus its own specific ones), the derived class is also a subclass of the base, i.e. all operations defined on the base is also available on the derived class.

Liskov Substitutional Principle (LSP)

Inheritance hierarchy

Based on object oriented design, we will define Vehicle base class to collect the common properties of all vehicles, and separate derived classes for cars, busses and trucks.

                   _________
                  |         |
                  | Vehicle |
                  |_________|
                       ^
                       |
         ______________|_______________
        |              |               |
    ____|____      ____|____       ____|____
   |         |    |         |     |         |
   |   Car   |    |   Bus   |     |  Truck  |
   |_________|    |_________|     |_________|

One can extend this hierarchy with a new derived class any time without disturbing the already existing (and well tested) other components.

The base class is the Vehicle class. For simplicity, we now give only the header file.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 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
  bool mot() const;    // the test 
  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

Derived classes are adding new attributes and extending the base class interface. However, the derived class constructor is responsible to provide the data for the constructor of the base class.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#ifndef CAR_H
#define CAR_Hhttps://ikelte-my.sharepoint.com/:v:/g/personal/gsd_inf_elte_hu/EbdkP9U_L_5GpCPbj6kogsoB_HDsPTAbqFUzcXUWNVQvYg?e=ouUyeK

#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
  bool mot() const { return Vehicle::mot() && _emis < 130; }
  std::string print() const { return std::sting("[car ")
                              + Vehicle::print() + 
                              /* car specific */ ;     }
private:
  double _emis; // gram/km CO2 emission
};
#endif /* CAR_H */

Similarly, for buses and trucks…

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
// bus.h
#ifndef BUS_H
#define BUS_H

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

class Bus : public Vehicle
{
public:
  Bus(std::string pl, int hpw, Owner own, int len, int pers)
   : Vehicle( pl, hpw, own), _length(len), _persons(pers) {}
  int length() const { return _length; }
  int person() const { return _persons; }
  // the test for Bus
  bool mot() const { return Vehicle::mot() && 
                            _persons/_length < 4; }
  std::string print() const { return std::sting("[bus ")
                              + Vehicle::print() + 
                              /* bus specific */ ;     }
private:
  int _length;  // total length
  int _persons; // max persons carried
};
#endif /* BUS_H */
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
// truck.h
#ifndef TRUCK_H
#define TRUCK_H

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

class Truck : public Vehicle
{
public:
  Truck(std::string pl, int hpw, Owner own, int tw, int al) 
  : Vehicle( pl, hpw, own), _totalWeight(tw), _axles(al) {}
  int totalWeight() const { return _totalWeight; }
  int axles() const { return _axles; }
  // the test for Truck
  bool mot() const { return Vehicle::mot() && 
                              _totalWeight/_axles < 5000.; }
  std::string print() const { return std::sting("[truck ") 
                              + Vehicle::print() + 
                              /* truck specific */ ;     }
private:
  int _totalWeight;  // kg
  int _axles;        // number of
};
#endif /* TRUCK_H */

Inheritance in C++

While in most object-oriented programming languages there is only one kind of inheritance – the interface extension, e.g. extend in Java – in C++ there are three different inheritance.

1. Public

This is the classical inheritance. The derived class’s public interface extends the base class’s interface. This is similar to inheritance in Java. All public methods of the base class is callable on the derived class. Also, upcast works: i.e. the objects of the derived class are converted to objects of the base class.

2. Protected

The public interface of the base class is visible in all derived classes for the derived class methods only. I.e. the method of a derived class can see the base class publics, but the outer word can not. The protected inheritance is transitive in the way that all derived will see the base interface in any step of inheritance.

3. Private

The public part of the base class is visible inside the methods of the immediate derived class, but are not visible outside of that derived class (including further derivations). This is inheritance for implementation, used when we do not want to extend the base class interface, but need its members (phyically) or its methods. It is almost the same that we have the base class object as an attribute (but wecan override the base class virtual functions). A typical case for private inheritance is to forbid copying of the object before C++11 using private inheritance from boost::noncopyable.

The kind of inheritance is marked by the appropriate keyword after the colon denoting the inheritance. When the keyword is missing the inheritance is public for struct and private for class.

Naturally, most cases we use public inheritance to express subtyping. Private inheritance happens for much specific cases for implementational purposes. Protected inheritance is used in rare cases.

Constructor and destructor

In the derived classes we have to provide the parameters for the base class constructors:

1
2
Truck( std::string pl, int hpw, Owner own, int tw, int al) 
 : Vehicle( pl, hpw, own), _totalWeight(tw), _axles(al) {}

The construction order of subobjects is (in recursive way):

  1. base classes (in order of declaration)

  2. attributes (in order of declaration)

  3. run of the constructor body

The destruction order is the reverse.

When a base class or any attributes of the derived class has a constructor or destructor, the compiler automatically creates a constructor or destructor for the derived class which calls the same method of the base(s).

When the programmer defines a constructor or destructor for the derived class the compiler weaves the base class method call to the defined special member function.

Copying of the objects (when possible) is done by default copying the base with its own (perhaps user defined) copy operation. Otherwise, the programmer can provide a user defined copy constructor or assignment operator which should call the similar copy operation on the base.

1
2
3
4
5
6
7
8
9
10
Derived( const Derived& rhs) : Base(rhs) // copy constructor 
{
  // copy of derived specific part
} 
Derived &operator=( const Derived& rhs) // assignment 
{
  Base::operator=(rhs);
  // copy of derived specific part
  return *this;
}

Move operations (since C++11) work in similar way, but the programmer should aware of the if it has a name rule.

Conversion between derived and base objects

Barbara Liskov in a 1987 conference keynote address entitled Data abstraction and hierarchy formulated the semantical connection between a subtype and its supertype in the following way (The work was common with Jeannette Wing):

If S is a subtype of T, then objects of type T may be replaced with objects of type S (i.e., objects of type S may substitute objects of type T) without altering any of the desirable properties of that program.

Let aware, that the LSP formulates a semantical connection. Object-oriented languages try to implement this connection with various synatactical rules.

Upcast

We call upcast when we have a derived object (or a pointer or reference to it), and it is converted to the base object (or a pointer or reference to). By the Liskov substitutional principle, this is safe, and automatic.

1
2
3
4
5
Derived d;

Base   b = d;    // ok, but slicing!!!
Base *bp = &d;   // ok,  but only Base part is accessible
Base &br = d;    // ok, but only Base part is accessible

Slicing happens when we assign a Derived object to Base: the derived part will be lost! Also, when we use a Base pointer or reference to a Derived object only the Base part of the derived object will be accessible.
The static type of b, *bp and br is Base.

Downcast

Downcast (converting a base object (pointer or reference) to derived is not safe, and not happens automatically.

1
2
3
4
5
6
7
8
9
Base  b;
Base *bp;
Base &br = ...

Derived   d = b;    // Error!
Derived *dp = bp;   // Error!
Derived &dr = br;   // Error!

Derived *dp = static_cast<Derived*>(bp);  // OK, but risky

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