C++ Coures

Polymorphism recap

Objectives

- Inheritance

- Polymorphism

- virtual method & overriding

- Pure virtual

- virtual destructor

- Multiple inheritance

- Diamond inheritance

- Virtual inheritance

Inheritance

class Insect {
  /* Implementation */  
};

class Fly : public Insect {
  /* Implementation */
}

class ButterFly : public Insect {
  /* Implementation */
}

class Bee : public Insect {
  /* Implementation */
}

One of the most prominent object oriented features is the ability to reuse code in a IS-A relationship

FlyButterFly and Bee are all derived of the Base class Insect, and hence both are considered to be a specialisation of insect. Meaning they can add/alter behaviour of insect to accommodate for the new types while leveraging Insect's existing implementation

Base class

 

 

 

 

Derived classes

 

 

Polymorphism

The advantage of having an IS-A relationship is the ability to use derived classes as a base class.

void capture(const Insect& insect)
{
   /* Implementation */
}

Bee maya;

capture(maya);

In other words, because Bee IS-A type of insect it is possible send it to a function/method that requires Insect.

However, such behaviour will be consistent only when references & pointers are used

void captureFast(Insect insect)
{
   /* Implementation */
}

captureFast(maya);  // Inadvisable and probably be a bug.

Virtual & Overriding

Altering & Adding behaviour is mostly performed via the virtual methods. Virtual keyword allows for behaviour to be overridden 

class Animal {
  virtual void speak() const { cout << "[...]" << endl; } 
};

class Dog : public Animal {
  virtual void speak() const { cout << "Wolf wolf" << endl; } // Overrides Animals behaviour 
};

class Lion : public Animal {
  virtual void speak() const { cout << "Roarrrrr" << endl; }  // Overrides Animals behaviour
};


void listenTo1(const Animal& animal) { animal.speak(); }
void listenTo2(Animal animal)        { animal.speak(); }

Lion simba;
listenTo1(simba);   // Output is "Roarrrrr"
listenTo2(simba);   // Output is "[...]"

As mentioned in previous slide, the behaviour is constant only when references & pointers are used

Virtual & Overriding - cont

The ability to use base classes type (such as Animal) as a placeholder for any of its deriving classes is called polymorphism

 

Polymorphism is deferred to runtime and this will only happen when using either a pointer or a reference, otherwise, the type will be deduced at compiler time (=static time).

void listenTo1(const Animal& animal) { animal.speak(); }
void listenTo2(Animal animal)        { animal.speak(); }

Lion simba;
listenTo1(simba);   // Runtime 
listenTo2(simba);   // Compile(Static) time

Pure virtual method

Sometimes, a base class will define the interface but can not have a meaningful implementation for one or more of it's methods. In this case we will use a pure virtual method.

In the above example it was improper to allow Animal to speak, we can fix it like that.

class Animal {
  virtual void speak() const = 0;  // <-- A Pure virtual method
};

class Dog : public Animal {
  virtual void speak() const { cout << "Wolf wolf" << endl; } // Overrides Animals behaviour 
};

class Lion : public Animal {
  virtual void speak() const { cout << "Roarrrrr" << endl; }  // Overrides Animals behaviour
};

Animal a; // ERROR: Can't instantiate Animal, it is an abstract class
Dog Lacy; // No problem

A pure virtual method is a method without implementation. A Class that has at least one pure virtual method is known as an abstract class and can not be instantiated.

UML - Class diagram 

Virtual destructor

When a class is supposed to be Base class, make sure that its destructor is virtual. Otherwise, it won't be safe to delete a base pointer pointing at derived class. The only exception is when destruction is trivial and not implemented.

#include <iostream>
using namespace std;

struct Insect {
   virtual ~Insect() { cout << "Dtor Insect" << endl; }
};

struct Bee  : public Insect {
   virtual ~Bee() { cout << "Dtor Bee" << endl; }
};

struct Bumblebee : public Bee {
   virtual ~Bumblebee() { cout << "Dtor Bumblebee" << endl; }
};

int main()
{
    Insect*  maya = new Bumblebee();
    delete maya;
}  
Output:
--------------
Dtor Bumblebee
Dtor Bee
Dtor Insect

In the above example, if Insect didn't have a virtual destructor, the behaviour would have been undefined, and in all likelihood only Insect's constructor would have been called.

Object properties

An object is at least 1 byte, if its type is empty (Doesn't have any attributes).

 

When the type(class/struct) is not empty the object will be the sum of its fields (give or take, some padding may be added by the compiler)

 

When the type(class/struct) has at least 1 virtual method, it's size will be increased by the size of a pointer (to a virtual table)

 

When the type is derived its size will also incorporate the sizes of all its base classes.

Object properties


struct Empty                     {};
struct NonEmpty                  { int i; };
struct WithVirtual               { virtual ~WithVirtual() {} };
struct Derived : public NonEmpty { int j; };

int main()
{
    cout << sizeof(Empty)       << endl;   // Output 1
    cout << sizeof(NonEmpty)    << endl;   // Output 4 
    cout << sizeof(WithVirtual) << endl;   // Output 4/8 (size of pointer)
    cout << sizeof(Derived)     << endl;   // Output 8  (sizeof i + sizeof j)
}

Multiple inheritance


struct HoofedAnimal { void gallop() const {...};  };
struct WingedAnimal { void fly() const {...};  };

struct Pegasus : public HoofedAnimal, public WingedAnimal {...};

In C++ it's also allowed to inherit the behaviour of multiple types, so there might be an IS-A towards more than one type.

Pegasus, as a winged horse, is both a HoofedAnimal and a WingedAnimal and can inherit both implementations 

Diamond Inheritance

struct Animal { string m_age; };
struct HoofedAnimal : public Animal { void gallop() const {...}; ... };
struct WingedAnimal : public Animal { void fly() const {...}; ... };

struct Pegasus : public HoofedAnimal, public WingedAnimal {};

Diamond inheritance -cont

In Diamond inheritance the top Base is being inherited twice, each of its attributes is duplicated and potentially has multiple values.

struct Animal          { 
   Animal(int age): m_age(age) {}
   int m_age; 
};
struct HoofedAnimal : public Animal { HoofedAnimal(int age): Animal(age / 2) {} };
struct WingedAnimal : public Animal { WingedAnimal(int age): Animal(age / 4) {} };

struct Pegasus : public HoofedAnimal, WingedAnimal {
  Pegasus(int age): HoofedAnimal(age), WingedAnimal(age)  {}
};

Pegasus  pega(20);

cout << pesa.m_age; // ERROR: won't compile ambiguous    
cout << static_cast<HoofedAnimal>(pega).m_age << endl;
cout << static_cast<WingedAnimal>(pega).m_age << endl;

source: cpp.sh/34meg

Multiple Inheritance Object's layout 

Virtual Inheritance

Virtual inheritance solves the problem in the case of diamond inheritance and derived class will only exist once

struct Animal { 
   Animal(int age): m_age(age) {}
   int m_age; 
};
struct HoofedAnimal : virtual public Animal { HoofedAnimal(int age): Animal(age / 2) {} };
struct WingedAnimal : virtual public Animal { WingedAnimal(int age): Animal(age / 4) {} };

struct Pegasus : public HoofedAnimal, WingedAnimal {
  Pegasus(int age): Animal(age), HoofedAnimal(age), WingedAnimal(age)  {}
};

Pegasus  pega(20);
cout << pega.m_age << endl;

Notice that Pegasus calls construction of Animal directly.

source: cpp.sh/7nga

 Object model

Multiple inheritance vs virtual inheritance 

Virtual inheritance - disadvantages

Slower - another layer of indirection 

More complex - in fact many consider multiple inheritance, something to be avoided.

Other languages have other ways of tackling similar problems via traits, mixing or interfaces to avoid Multiple inheritance.

 

 

Virtual inheritance - considerations

- Construction of object with virtual inheritance is , virtual class first, than regular left to right depth first construction occurs.

 

 

struct Animal { 
   Animal(const string& name):m_name(name) { cout << "ctor Animal(" << name << ")" << endl;}
   ~Animal() { cout << "dtor Animal (" << m_name << ")" << endl;}
   
   string m_name;
};

struct HoofedAnimal : virtual public Animal {
    HoofedAnimal(): Animal("HoofedAnimal")  { cout << "ctor HoofedAnimal" << endl;} 
    ~HoofedAnimal() { cout << "dtor HoofedAnimal" << endl; }
};

struct WingedAnimal : virtual public Animal { 
    WingedAnimal(): Animal("WingedAnimal") { cout << "ctor WingedAnimal" << endl; } 
    ~WingedAnimal() { cout << "dtor WingedAnimal" << endl; }
};

struct Pegasus : public HoofedAnimal, WingedAnimal {
  Pegasus(): Animal("Pegasus") { cout << "ctor Pegasus" << endl;}
  ~Pegasus() { cout << "dtor Pegasus" << endl; }
};


Pegasus* pega = new Pegasus;
delete pega;
Output
-------------------
ctor Animal(Pegasus)
ctor HoofedAnimal
ctor WingedAnimal
ctor Pegasus
dtor Pegasus
dtor WingedAnimal
dtor HoofedAnimal
dtor Animal (Pegasus)

Virtual inheritance - considerations

Downgrading using C style cast of static_cast is not possible, dynamic_cast should be used **

struct Base                               { int x1; };
struct Derived1 : virtual Base            { int x2; };
struct Derived2 : Derived1, virtual Base  { int x3; };

void displayDiff( const Derived1& d )
{
    const Derived1* pDerived = &d;
    const Base*     pBase    = &d;
    std::cout << (void*)pDerived << ", " << (void*)pBase;
    const char* cd = reinterpret_cast<const char*>(pDerived);
    const char* cb = reinterpret_cast<const char*>(pBase);
    std::cout << "    diff " << (cd-cb) << "\n";
}

Derived1 d1;
displayDiff(d1);

Derived2 d2;
displayDiff(d2);

At compile time the offset is unknown between base class and its derivatives.

Output
------------
0x79da23258d10, 0x79da23258d1c    diff -12
0x79da23258d20, 0x79da23258d30    diff -16

C++L06

By perplexedpigmy