Polymorphism recap
- Inheritance
- Polymorphism
- virtual method & overriding
- Pure virtual
- virtual destructor
- Multiple inheritance
- Diamond inheritance
- Virtual 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
Fly, ButterFly 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
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.
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
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
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.
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.
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.
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)
}
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
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 {};
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
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
Multiple inheritance vs virtual inheritance
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.
- 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)
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