Inheritance


Inheritance

oop



Learning Objectives

  • Represent inheritance hierarchies in UML and C++ and decide when to use inheritance and when to use composition
  • Explain how to use public, protected and private inheritance for code reuse in C++
  • Make good use of constructor chaining in C++ inheritance hierarchies
  • Explain the meaning of the OOP terms polymorphism and dynamic binding and describe how they are implemented in C++
  • Make use of abstract base classes and (pure) virtual functions in C++ inheritance hierarchies
  • Make use of member function hiding in C++ inheritance hierarchies
  • Define multiple inheritance hierarchies in C++ and use virtual base classes when appropriate.

Implementing Inheritance

By using inheritance, we can cause a class to inherit data members and/or member functions from another class. In C++, we refer to the class:

  • inheriting from as the base class or super class
  • inheriting to as the derived class or sub class

To for an introductory example let’s define a single class to store information and perform calculations about different geometric shapes.

shape.h
class Shape {
  public:
    Shape() {};
    float GetArea() { return _area; }
    float GetPerimeter() { return _perimeter; }
  private:
    float _area;
    float _perimeter;
};

The Shape case has one default constructor (Shape() {};) and two public functions, as well as two private data members. As this class stands, there is no way of computing the area or perimeter of the shape, since this detail depends on the specific shape being presented. Therefore, this class is a ‘general’ class for describing shapes and we probably wouldn’t want to define any instances of it as it stands.

Now suppose that we want to define classes for specific shapes, such as:

  • A circle with a radius but also an area and a perimeter (i.e. circumference)
  • A square with a side length but also an area and a perimeter

Therefore, two of the data members are the same for both circles and squares. Rather than duplicating the code, it can be reused by inheriting from Shape when defining the two new ca=lasses, Square and Circle. The code can be modified as so:

shape.h with additions
class Shape {
  public:
    Shape() {};
    float GetArea() const { return _area; }
    float GetPerimeter() const 
    { return _perimeter; }
  protected:
    float _area;
    float _perimeter;
};

class Circle: public Shape {
  public:
    Circle(float r=0) {_radius = r;}
    void ComputeArea();
    void ComputePerimeter();
  protected:
    float _radius;
};

class Square: public Shape {
  public:
    Square(float s) {_side = s;}
    void ComputeArea();
    void ComputePerimeter();
  protected:
    float _side;
};

The line class Circle: public Shape is the location where inheritance is defined for the Circle class. In essence it means “the Circle class publicly derives from the Shape class”. Inheritance is indicated by the colon character : after the derived class name. The same applies to the Square class.

Note that the two data members in Shape are now protected rather than private. The difference between protected and private members is that private members in a base class are never inherited by the derived class, whereas protected members are. Apart from this they are identical.

Types of Inheritance

The public keyword in the inheritance means that we are using public inheritance. Alternatives to public inheritance are private or protected inheritance, but these are not widely used. The precise definitions of the different types of inheritance are:

  • Public inheritance keeps all of the public members of the base class public in the derived class
  • Protected inheritance makes all of the public members of the base class protected in the derived class
  • Private inheritance makes all of the public members of the base class private in the derived class.

UML Notation for Inheritance

Inheritance can be represented using UML class diagrams. In the example below, the inheritance from Circle and Square to Shape is indicated by arrows joining the derived class to the base class.

UML DIAGRAM

Public and protected members contained in the base class, although not shown in the derived classes, are available to those classes. A # symbol is used to denote protected members in UML (recall that + denotes public and - denotes private)

Constructor Chaining

Let’s consider another example, which contains classes for storing information about doctors and consultants at a hospital. The gender, name, title and basic salary information needs to be stored about doctors. For consultants, the same information needs to be stored but in addition to their specialism. There is a clear relationship between doctors and consultants as they share four of the same data members. Rather than duplicating code in the two classes, inheritance can be used to reuse code.

doctor.h
enum GenderType {Male, Female};

class Doctor {
  public:
    Doctor(GenderType g, string n)
    {
      _gender = g;
      _name = n;
      _title = "Dr";
      _basicSalary = 30000;
    }
    void Display () const
    {
      cout << _title << "  " << name << endl;
      if (_gender == Male)
        cout << "Male" << endl;
      else
        cout << "Female" << endl;
      cout << "Basic salary = "
           << _basicSalary << endl;
    }
  protected:
    GenderType _gender;
    string _name;
    string _title;
    float _basicSalary;
};

In this example, the following Consultant class inherits (or derives) from Doctor using public inheritance.

consultant.h
class Consultant : public Doctor
{
  public:
    Consultant(GenderType g, string n, 
               string s): Doctor(g, n)
    {
      _specialism = s;
      if (_gender == Male_)
          _title = "Mr";
      else
          _title = "Ms";
      _basicSalary = 70000;
     }
     void Display()
     {
       Doctor::Display();
       cout << "Specialism = "
            << _specialism << endl;
     }
  protected:
    string: _specialism;
};  

The new concept constructor chaining is illustrated above, Consultant( ... ) : Doctor( ... ). After the Consultant constructor function header, there is a colon followed by a call to the Doctor constructor. This causes the base class (i.e Doctor) constructor to be called whenever the derived class (i.e . Consultant) constructor is called. In order words, the calls to the class constructors are ‘chained’ up the inheritance hierarchy. Constructor chaining happens anyway for default constructors (i.e those with no arguments), but if we want it to happen for other constructors (i.e. those with arguments) we must explicitly define constructor chaining like this.

The below UML diagram shows the relationship between the Doctor and Consultant classes.

UML DIAGRAM

Member Function Overriding

The earlier example, also illustrated another new concept called member function overriding or member function hiding. See how there are definitions for the Display() member function in both the Doctor class and the Consultant class. Normally, the Doctor Display() function would be inherited by the Consultant, but in this case it would be incorrect as it doesn’t display the consultant’s _specialaism. Therefore Consultant defines its own version of Display(), which hides the base class version.

Note that we can still call the base class version from the derived class version using the scope operator ::, (Doctor::Display();).

Abstract Base Classes and Concrete Classes

In this chapter there have been two different examples of inheritance from Circle/Sqaure to Shape, and from Consultant to Doctor. Although clearly the use of inheritance is appropriate in both of these case, they are slightly different. In the Consultant/Doctor example, it is possible, even likely, that we will want to create instances of both Consultant and Doctor, since they are both types of clinician at the hospital. However, for the Shape example we will never want to create an instance of the Shape class - it only exists so that we can derive real specific shapes from it. Based on this distinction, now let’s introduce a couple of new terms:

  • Abstract base class only exists to derive new classes form it (The Shape class is an example of an abstract base class)
  • Concrete class exists to create instances of itself (The Square Circle Doctor Consultant classes are all examples of concrete classes)

Virtual Functions

An inheritance hierarchy can be thought of as defining a type/subtype relationship between classes, e.g. a circle is a type of shape. A virtual function defines a type dependent operation within an inheritance hierarchy.

To illustrate the use of virtual functions, let’s modify the shape example introduced earlier:

shape.h modified
class Shape {
  public: 
    Shape() {};
    float GetArea() const { return _area }
    float GetPerimeter() const
    { return _perimeter; }
    // pure virtual functions
    virtual void ComputeArea() = 0;
    virtual void ComputePerimeter() = 0;
  protected:
    float _area;
    float _perimeter;
};

...

The keyword virtual specifies that the ComputeArea() and ComputePerimeter() functions are virtual functions, which represent type dependent operations in the inheritance hierarchy. In fact, these two functions are pure virtual functions. A pure virtual function is specified by adding an assignment to zero after the virtual function signature. A pure virtual function, as well as being a type dependent operation, means that the class that contains it will be an abstract base class, i.e. it will not be possible to create an instance of it. Attempting to create an instance of it will result in a compilation error.

In addition, all classes that derive from a class containing a pure virtual function will also be abstract base classes unless they provide an implementation of the virtual function. In the shapes example, it is now a requirement for both Circle and Square to implement ComputeArea() and ComputePerimeter(), otherwise it will not be possible to create any Circle or Square instances.

The below UML diagram shows how the Shape.h relationships change now we have defined pure virtual functions. Displaying the member function names in italics in Shape indicates that these are virtual functions. Furthermore, the text <<abstract>> above the class name indicates that this is an abstract base class.

UML DIAGRAM

Virtual functions are one way in which the concept of polymorphism is implemented in C++.

Overriding Virtual vs. Non-Virtual

Virtual functions provide a way of specifying different implementations for a single member function at different points in an inheritance hierarchy. However, similar functionality can be produced by overriding a member function without using the virtual key word. i.e. if we override an ‘ordinary’ (non-virtual) base class function in the derived class it will hide the base class function.

For example, the two implementations of an inheritance hierarchy are below, the first using virtual functions and the second using member function overriding. In both implementations an instance of the base or derived class will call their ‘own’ version of the function fn.

class base {  
  ...
  public:
    virtual float fn();
}

class derived {
  ...
  public:
    float fn();
}

When a non-virtual member function is overridden the choice of which implementation from the inheritance hierarchy to use is always made at compile-time. With virtual functions this choice can be made at run-time.

class base {
  ...
  public:
    float fn();
}

class derived {
   ...
   public:
     float fn();
}

The choice of which implementation to use is known as binding. If binding is made at run-time, it is known as dynamic binding. Dynamic binding is used whenever a virtual member function is called through a pointer. The following code illustrates the use of dynamic binding:

Shape *sq = new Square(4.5);
Shape *c = new Circle(2.7);
sq->ComputeArea();
c->ComputeArea();

Note that -> is shorthand for dereferencing a pointer then selecting a member, e.g. the following are the same: (*sq).ComputeArea() and sq->ComputeArea()

Here, the instances sq and c both have a static (i.e. compile time) of Shape*. However, since Square and Circle both derive from Shape, the dynamic type of sq and c could b either Shape*, Square* or Circle*. Precisely which one of these three possibilities they are will not be decided until run-time. In other word, binding of sq and c is dynamic.

Dynamic binding is only used if the virtual function is accessed through a pointer as in the example above. If sq and c had types Square and Circle rather than Shape* then dynamic binding would not be used. In this case they would only have a static type and no dynamic binding would be necessary.

Polymorphism is C++ is related to binding and can be split up into two separate terms:

  • Run-time polymorphism is where the choice of operation being made at run-time and involves dynamic binding (e.g. virtual functions)
  • Static polymorphism is where the choice of operation being made at compile-time (e.g. member function overriding)

Inheritance vs Composition

Composition refers to making one class a data member of another class. Inheritance also results in the members of one class (the base class) being made available to another class (the derived class). All the examples in this chapter could have been implemented using composition rather than inheritance, by including a data member of type Shape in the Circle and Square classes, or add a Doctor member to the Consultant class.

To decide between using inheritance and composition the following rules can be applied. If the relationship between two classes can be described as an:

  • is-a relationship, then it is best to use inheritance.
  • has-a relationship, then it is best to use composition.

For example, in the shapes example, a Triangle is a Shape, so inheritance is the best option. Whereas, with the vectors example, we cannot say that a Vector is a Point, but we can say that a Vector has a Point, so in this case composition is the appropriate mechanism to use.

Single and Multiple Inheritance

In all of the examples so far, each class has only inherited from a single base class. This is know as single inheritance. In C++ it is permissible to inherit from multiple base classes: this is referred to as multiple inheritance.

To illustrate this concept of multiple inheritance, let’s use a student database program example, which stores information about both overseas and UK students. First look at the UML class diagram below. There are three base classes: UKCitizen Student OverseasCitizen. Each has their own data members and corresponding mutator and inspector member functions. The UKStudent class derives from both Student and UKCitizen, and OverseasStudent derives from both Student and OverseasCitizen. This is logical since a UK student is both a student and a UK citizen, and a overseas student is both a student and an overseas citizen.

UML DIAGRAM

The Student class has a pure virtual function called Fees(): all students must pay fees, but the way the fees are computed varies. Therefore, Student is an abstract base class. However, UKCitizen and OverseasCitizen are not - they are normal base classes. An excerpt from the code that implements this inheritance hierarchy is shown below:

Student.h
class OverseasStudent: public OverseasCitizen,
                       public Student { 
...
}

class UKStudent: public UKCitizen,
                 public Student { 
...
}

Multiple inheritance is specified by simply listing multiple vase classes when inheriting, and separating them by commas. Using this inheritance hierarchy is an efficient way to represent the different types of information stored about a student in the database, and permits good code reuse.

Virtual Base Classes

Sometimes multiple inheritance can cause problems. This can be illustrated by the following example, which contains a number of classes to represent different types of animal.

Look at the UML diagram below. There is a base class called Animal, which contains a protected data member called _expectedLifeSpan. Two other classes derive from Animal: Predator Endangered. Both of these classes inherit the _expectLifeSpan data member. The SnowLeopard class multiply inherits from both Predator and Endangered, since snow leopards are endangered predators. Therefore, SnowLeopard would normally inherit two data members called _expectedLifeSpan. This would make any reference to _expectedLifeSpan in SnowLeopard ambiguous, an cause a compilation error.

UML CLASS DIAGRAM

This answer to this problem lies in the use of a virtual base class. If we need to define a hierarchy with such ambiguous inheritance we can define the root base class as a virtual base class, and then only copy one of each multiply-inherited member will be created. To make an Animal a virtual base class we simply add the keyword virtual when defining inheritance from it. For example consider the code below:

Animal.h
class Animal
{
  public:
    Animal() {};
    int GetExpectedLifeSpan()
    { return _expectedLifeSpan; }
    void SetExpectedLifeSpan(int val)
    { _expectedLifeSpan = val; }
  protected:
    int _expectedLifeSpan;
};

class Predator: public virtual Animal
{
  public:
    Predator() {};
    string GetMainPrey()
    { return _mainPrey; }
    void SetMainPrey(string val)
    { _mainPrey = val; }
  protected:
    string _mainPrey;
}

class Endangered: public virtual Animal
{
  public:
    Endangered() {};
    int GetNumLeft() { return _numLeft; }
    void SetNumLeft(int val) { _numLeft = val; }
  protected:
    int _numLeft;
}

class SnowLeopard: public Endangered,
                   public Predator
{
  public:
    SnowLeopard() {};
}

Note that virtual is only added to the Animal base class, not when SnowLeopard is inheriting from Endangered and Predator.

This inheritance problem will only occur if the inheritance hierarchy forms a ‘diamond-like’ shape. Fortunately such cases are quite rare, but it is good to know about this potential problem.


return  link
Written by Tobias Whetton