The C++ Programming Language

Part 7: Inheritance

Inheritance is the capability of one class to inherit the attributes and behavior of another class. It is one of the most important features of Object-Oriented Programming.

The class that acts as base for the inheritance is called base class or parent class.

The new class is called derived class or child class.

Defining a derived class

The basic syntax for deriving a class from another class is:

class subclass_name : access_specifier base_class_name {
    // class declaration
};

The access_specifier in combination with the access_specifiers used in the declaration of the base class determines what the derived class can directly access from the base class. We will cover this later.

Here is an example of class derivation:

// base geometry shape
class geom_shape {
};

// a triangle is a shape
class tria : public geom_shape {
};

// a quad is also a shape
class quad : public geom_shape {
};

The basic concept of inheritance is the is-a relationship between parent-child classes. The child class is a specialization of the parent class. Although it is the same as the parent it adds some characteristics that make it different from other types. A triangle and a quadrilateral are both geometric shapes and thus share some characteristics, but they also have some distinct attributes that make them different.

In this example it becomes clear how inheritance works:

#ifndef __inheritance_h__
#define __inheritance_h__

enum g_type { t_tria, t_quad };

class geom_shape {
    g_type m_type;
public:
    geom_shape(g_type t):m_type(t) {
    }
    g_type type() const {
        return m_type;
    }
    int num_sides() {
        switch (m_type){
        case t_tria:
            return 3;
        case t_quad:
            return 4;
        }
        return 0;
    }
};

class tria : public geom_shape {
public:
    tria() : geom_shape(t_tria) {
    }
};

class quad : public geom_shape {
public :
    quad() : geom_shape(t_quad) {
    }
    int num_diagonals() {
        return 2;
    }
};
#endif // __inheritance_h__

Here we have some functionality in the base class and some specialized in the derived.

The constructor of the base class accepts the type of the object to store it in the member variable m_type. Then it uses this type to answer the number of sides an object has. The derived classes have no arguments in their constructors, but they call the base class constructor passing their respective types. This is the first of the differentiations we have. Apart from that the quad class gives an extra option to query the number of diagonals an object has.

Modifying behavior

The derived class inherits attributes and behavior from parent. We can have a function in the derived class with the same prototype as in the base class. This allows us to change the behavior of the class. In the above example we could rewrite the num_sides function in each class and let it return the correct number:

class geom_shape {
public:
    int num_sides() {
        // default returns 0
        return 0;
    }
};

class tria : public geom_shape {
public:
    int num_sides() {
        return 3;
    }
};

class quad : public geom_shape {
public :
    int num_sides() {
        return 4;
    }
};

Our calling code remains unchanged. Only the class definitions changed. This allows us to define different behavior for every class when we need.

Virtual functions

Using a pointer or a reference to access an object is something we have seen in before. Inheritance is creating another use case requiring a different solution. A derived class IS-A base class too. So, we can access it through a pointer or a reference to the base class. The following is valid code:

geom_shape* ptr = new quad;
Having a pointer of the base class and assign it a pointer of the derived class. The question is what would happen if we called num_sides using this pointer?
std::cout << "sides of ptr=" << ptr->num_sides() << "\n";
This calls the base class version of the program despite the fact that it is a quad. C++ introduced the concept of virtual functions. This allows us to overcome the problem we just encountered and call the correct function regardless of the type specified by the pointer.

Syntax

The syntax of the virtual function declaration is very simple. We just have to prepend the keyword virtual in the start of the function declaration.

virtual returnType function(argumentList);

How it works

Every object has a so-called virtual table. There the compiler stores the addresses of the virtual functions. So, when it generates the call it first checks the virtual table and if the function is in it the compiler uses it for the call. If the function is not there, the compiler selects the function based on the object type and when we have a base class pointer it calls the base class function.

Example

We have modified the code and now it calls the function we expect it would:

class geom_shape {
    virtual int num_sides() {
        // default returns 0
        return 0;
    }
};

class tria : public geom_shape {
    virtual int num_sides() {
        return 3;
    }
};

class quad : public geom_shape {
public :
    }
    virtual int num_sides() {
        return 4;
    }
};

Here is the output when running the code:

type of q=1      -- quad
sides of q=4     -- direct call
type of t=0      -- tria
sides of t=3     -- direct call
sides of ptr=4   -- calling quad through a pointer to the base class!

Virtual destructors

When we use pointers of the base class to store pointers to objects of a derived class it is good to have virtual destructors. This way if we need to call delete to destroy an object the compiler will generate the correct call.

virtual ~geom_shape() {}

Calling base class functions

When we need to call a base class function, usually a virtual function, we can do so using the base class name followed by the scope operator (::) and the name of the function.

In virtual functions we may need to call the same function from the base class to perform default actions. Then we may proceed with the derived class specialization:

class geom_shape {
public:
    virtual void print() {
        std::cout << m_type;
    }
};

class tria : public geom_shape {
public:
    virtual void print() {
        geom_shape::print();
        std::cout << " tria\n";
    }
};

class quad : public geom_shape {
public :
    virtual void print() {
        geom_shape::print();
        std::cout << " quad\n";
    }
};

If we call the print function on any of the derived classes, it will first call the base class to do the default and then do its own stuff. This is very useful in big and complex classes and allows us to reuse code instead of rewriting it.

Abstract classes

Abstract classes are designed to act as interfaces and cannot be instantiated directly. This means we cannot create any object of this type. They can only be accessed via pointer to a derived class object.

How to create an abstract class

A class the contains at least one pure virtual function is an abstract class. A pure virtual function is a virtual function that has no body but is assigned to zero (0) when declared like this:

virtual int num_sides() = 0;

This inserts a NULL pointer in the virtual table, which is not allowed for class instances, objects, and so we cannot directly create an object of the class.

These are the basic characteristics for abstract classes:

No Instances: We cannot instantiate any objects of an abstract class.

Pointers and References: We can have pointers and references to an abstract class

Derived Classes: A derived class must override the virtual function, otherwise it becomes abstract as wel

Constructors/Destructors: Abstract classes can have constructors and destructors.

Use of abstract classes

An abstract class is useful when w need to create an abstract generalization to describe a set of objects. In our example we have trias and quads which have the abstract type geom_shape. It is obvious that having an object of the type geom_shape should be avoided because it is too general to be useful.

The base class can still be used to hold the common attributes and behavior for the derived classes as well as declare functions that can be shared among derived classes but perform according to the specific object.

Here are the classes with virtual and pure virtual functions:

class geom_shape {
    g_type m_type;
public:
    geom_shape(g_type t):m_type(t) {
    }
    g_type type() const {
        return m_type;
    }
    virtual int num_sides() = 0;
};

class tria : public geom_shape {
public:
    tria() : geom_shape(t_tria) {
    }
    virtual int num_sides() {
        return 3;
    }
};

class quad : public geom_shape {
public :
    quad() : geom_shape(t_quad) {
    }
    virtual int num_sides() {
        return 4;
    }
    int num_diagonals() {
        return 2;
    }
};

The geom_shape cannot be instantiated but it still can act as an interface to any object derived from it:

geom_shape* ptr = new quad;
std::cout << "sides of ptr=" << ptr->num_sides() << "\n";
delete ptr;

The sample code has remained unchanged. We have a base class pointer but we created a derived class object. Then via this pointer we call the virtual function and the compiler generates the correct call.

From base class to derived class, RTTI

In C++ we can get a derived class pointer when given a base class one. That can be done safely and if the pointer is not of the class we expected it will return us a NULL pointer which we can safely check and avoid a crash.

The mechanism is called Run-Time Type Information. This feature allows the type of an object to be determined during the execution of the program.

In C++ we have two mechanisms to retrieve the actual class information of an object. These are typeid and dynamic_cast.

typeid

The typeid operator is used to identify the class of an object at runtime. It returns a reference to a std::type_info object, which provides information about the type. Here's an example:

geom_shape* ptr = new tria;
std::cout << typeid(*ptr).name() << "\n";
delete ptr;

dynamic_cast

The dynamic_cast operator is used for down casting pointers or references to more specific types within a class hierarchy. It performs a runtime check to ensure the cast is valid and returns a pointer or reference of the converted type if successful. Here's an example:

geom_shape* ptr = new tria;
tria* tptr = dynamic_cast<tria*>(ptr);
if (tptr)
    std::cout << "triangle\n";

quad* qptr = dynamic_cast<quad*>(ptr);
if (qptr)
    std::cout << "quad\n";

std::cout << tptr << ", " << qptr << "\n";
delete ptr;

Considerations and Limitations

RTTI provides a powerful tool for managing polymorphic class hierarchies and performing safe runtime type checks and conversions. However, it introduces overhead and can lead to more complex code maintenance.

Access control

Access control is a fundamental concept in object-oriented programming. It helps us manage the visibility and accessibility of a class members to its descendants and the rest of the world.

There are three levels of visibility controlled by the access specifiers. These are: public, protected and private. They are applied inside a class to limit access to its members and during inheritance to limit how derived class can access the base class.

Access specifiers in a class

Here is how access specifiers modify accessibility inside a class:

  1. Public: when members are declared as public they are accessible from anywhere in the program. Any function or class can access them.
  2. Protected: protected members can only be accessed by the class members, its descendants and friends. No other class of function can access them.
  3. Private: private members can be accessed only by member functions and friends. No other class or function, including derived classes can access them.

Access specifiers in inheritance

Inheritance in C++ can be specified as public, protected and private. So far we have seen public inheritance in our examples.

  1. public inheritance maintains access levels between base and derived class.
  2. protected inheritance upgrades access level making the public and protected members of the base class protected in the derived class.
  3. private inheritance makes the public and protected members of the base class private in the derived class.

Here are some examples to clarify things. First we are going to create our base class:

class base {
private:
    int priv_member;
protected:
    int prot_member;
public:
    int publ_member;

    base() :priv_member(1), prot_member(2), publ_member(3) {
    }
    void fun_base() {
        std::cout << "base:" << priv_member << ", " << prot_member << ", " << publ_member << "\n";
    }
    friend void some_friend(base& b);
    friend class good_friend;
};

Public Inheritance.

class der_publ :public base {
public:
    der_publ() {}
    void fun_publ() {
        fun_base();
        std::cout << "publ:" << prot_member << ", " << publ_member << "\n";
    }
};

The base class members are accessible normally and we can print their values without problem. Now let us see what the rest of the world can access:

der_publ pbl;

// all public members of base are accessible
pbl.fun_base();
std::cout << pbl.publ_member << "\n";

Members can be accessed as if they were declared in this class.

Protected Inheritance:

class der_prot :protected base {
public:
    der_prot() {}
    void fun_prot() {
        fun_base();
        std::cout << "prot:" << prot_member << ", " << publ_member << "\n";
    }
};

The derived class continues to have access to members based on their declaration and not the inheritance type. Things change for the rest of the world though:

der_prot prt;

// no base functionality is available directly
prt.fun_prot()

Now only the derived class functions are visible to the rest of the world.

Private Inheritance:

class der_priv :private base {
public:
    der_priv() {}
    void fun_priv() {
        fun_base();
        std::cout << "priv:" << prot_member << ", " << publ_member << "\n";
    }
};

Accessibility from the derived class is unchanged.

der_priv prv;

// no base functionality is available directly as well
prv.fun_priv();

Not much of difference for the rest of the world. Private is stronger than protected but the result is the same.

Friends: We have declared a friend function and a friend class in the base class. These have full access to all the members of the class regardless of access specifiers:

void some_friend(base& b) {
    // we have access in the friend
    std::cout << b.priv_member << "," << b.prot_member << "," << b.publ_member << "\n";
}
class good_friend {
public:
    good_friend(base* b=nullptr){
    }

    void show_all(base* b = nullptr) {
        if (b == nullptr)
            return;
        std::cout << b->priv_member << "," << b->prot_member << "," << b->publ_member << "\n";
    }
};

Multilevel Inheritance

Deriving a class from another derived class is called multilevel inheritance. The new class inherits attributes and behavior not only by its base class, but also from the class above that. Multilevel inheritance allow us to specialize our classes at each level. Here is a small class hierarchy demonstrating multilevel inheritance:

// multilevel inheritance
class vehicle {
public:
    vehicle() { std::cout << "vehicle\n"; }
    virtual ~vehicle() {}
    virtual void print() = 0;
};
// car is a vehicle
class car :public vehicle {
public:
    car() { std::cout << "car\n"; }
    virtual ~car() {}
    virtual void print() {
        std::cout << "i am a car\n";
    }
};
// sports_car is a car, and a vehicle
class sports_car :public car {
public:
    sports_car() { std::cout << "sports car\n"; }
    virtual ~sports_car() {}
    virtual void print() {
        car::print();
        std::cout << "i am a sports car\n";
    }
};

Creating a sports_car object will make both car and vehicle constructors run.

Multiple inheritance

A class can be derived from more than one base class. This is called multiple inheritance. It inherits attributes and behavior from all of them. Here is a simple example:

// multiple
class base_a{
public:
    base_a() { std::cout << "base_a\n"; }
    virtual void print() {
        std::cout << "base_a\n";
    }
};

class base_b {
public:
    base_b() { std::cout << "base_b\n"; }
    virtual void print() {
        std::cout << "base_b\n";
    }
};

class class_c :public base_a, public base_b {
public:
    class_c() {}
    virtual void print() {
        std::cout << "class_c\n";
    }
};

In this example class_c is both base_a and base_b. When we create an object of class_c both base classes constructors run.

The diamond problem

This problem occurs when the two base classes have a common ancestor. Here is an example:

class serializable {
public:
    serializable() { std::cout << "serializable\n"; }
    void save_to_disk() {}
};
class base_a : public serializable ...
class base_b : public serializable ...
class class_c :public base_a, public base_b ...

The next diagram shows the diamond construct.

diamond

This is a very common issue with multiple inheritance and can lead to having two instances of the serializable class members when we instantiate a class_c object. It is easy to see that the constructor of the serializable class runs twice, once for base_a and one for base_b. so we end up with twice as much data which may be crucial in program with big amounts of data.

Virtual Inheritance

The solution to the problem is virtual inheritance. This is achieved using the virtual keyword. Here is how:

class serializable ...
class base_a : virtual public serializable ...
class base_b : virtual public serializable ...

This allows the creation of only one copy of the serializable class resolving all issues.

Summary

In this part we covered inheritance. Specifically, we covered these subjects:

The C++ Programming Language