C++ Inheritance
June 11, 2020 by Jane

This is an addition to Object-Oriented Concepts

Another relevant post The Diamond Problem and Virtual Inheritance

 

Abstract Classes:

C++ doesn't have the keyword "abstract" like in some languages, but it does have the concept of a pure virtual member function - a function without an implementation; a function purely meant for inheriting, to be implemented by derived classes. Classes that have a pure virtual member function are the same as abstract classes in other languages, since the class cannot stand on its own.

 

To declare a pure virtual member function, assign the function declaration to 0.

E.g.

virtual double perimeter() const = 0;

 

Interfaces:

Similarly, C++ doesn't have the keyword "interface" like some languages, but commonly, if a class has only pure virtual member functions, it is equivalent to an interface.

 

Polymorphism:

class P {

public:

   void print() { cout << "Inside P"; }

};

class Q : public P {

public:

   void print() { cout << "Inside Q"; }

};

class R: public Q {};

int main(void) {

  R r;

  r.print();

}

What gets printed? Inside Q is correct. The R class doesn't have a print() method, so it looks up the inheritance hierarchy and the one that's closer to class R is used.

 

Virtual Function Tables (vtables):

Virtual function tables enable polymorphism to happen. Behind the scenes each virtual function implemented by the class is placed in a vtable. Each object has a vtable pointer that points to the virtual function appropriate for the object. 

Instances of the same class point to the same vtable. Derived instances have their own vtables.

E.g.

class Employee {

public:

    string name;

    int salary;

    int get_salary() { return salary; }

    string get_name() { return name; }

};

class Manager : public Employee {

private:

    int get_bonus() {return 500;}

public:

    int get_salary() { return salary + get_bonus(); };

};

Each instance of Employee consists of:

  • name
  • salary
  • vtable pointer
    • points to vtable for class Employee which contains:
      • Employee::get_salary
      • Employee::get_name

Each instance of Manager consists of:

  • name
  • salary
  • vtable pointer
    • points to vtable for class Manager which contains:
      • Employee::get_name
      • Manager::get_salary

 

Object Slicing:

Object slicing happens when assigning a derived instance to a base class. 

E.g. 

class A {

    int m_i;

    virtual void print() { cout << "A" << endl; }

};

class B: public A {

    int m_j;

    virtual void print() { cout << "B" << endl; }

};

void callMe(A obj) {

    obj.print();

}

int main() {

    A a;

    B b;

    callMe(a);

    callMe(b);

}

This will print "AA" because once b is passed to the function, the derived class object is sliced off and all the data members inherited from base class are copied. So the overrided print() is not copied, nor is m_j.

To avoid slicing, do not assign derived classes to base classes, instead use pointers.

E.g.

void callMe(A& obj) {

    obj.print();

}

 

void callMe(A* obj) {

    obj->print();

}

int main() {

    A a;

    B b;

    callMe(a);

    callMe(b);

 

    A *pa = new A();

    B *pb = new B();

    callMe(pa);

    callMe(pb);

}

This will print "AB" and "AB" because we're passing a reference to the callMe() function.

Similarly:

int main() {

    A *a = new A();

    A *b = new B();

    a->print();

    b->print(); 

 

    callMe(a);

    callMe(b);

}

This will also print "AB" and "AB". This is because when the pointer is initialized the vtable pointer uses the vtable for object B and at run time the correct method is called.

 

Dynamic Casts:

The dynamic_cast operator requires a type as a template parameter, followed by a parameter that must be a pointer or reference. To use a dynamic cast, objects must belong to a class with at least one virtual function. Also note that RTTI must be enabled for this to work.

E.g. Assuming Manager and Employee take a name and salary as constructor parameters. Manager is a derived class of Employee.

Employee * e = new Manager("Sarah", 3000); // assignment of manager to employee is an upcast

Manager * m = dynamic_cast<Manager*>(e); // dynamic cast from employee to manager is a downcast

If the dynamic cast succeeds, m is a valid pointer, otherwise m is a nullptr. We always need to check if the cast succeeds, then proceed.

If Employee e was a reference instead of pointer and the dynamic cast fails, a bad_cast exception will be thrown since there's no nullptr for references.

 

Typeid Operator:

Another way to know the type of an object is to get its typeid. 

E.g.

#include <typeinfo>

int main() {

    Employee e;

    cout << typeid(e).name() << endl; // will print "class Employee"

    cout << typeid(Employee).name() << endl; // will also print "class Employee"

 

    Employee * eptr;

    cout << typeid(eptr).name() << endl; // will print "class Employee *"

    cout << typeid(&e).name() << endl; // will also print "class Employee *"

    cout << typeid(Employee*).name() << endl; // will also print "class Employee *"

}

*In general it's not good design to use typeid to query the type. We should use polymorphism as much as possible. If the code requires using typeid very often, perhaps it's time to re-examine the relationships of the objects in the program.