Notes on Inheritance & Polymorphism in C++

These are some of my notes on trying to understand Inheritance and Polymorphism in C++.

As is standard in Object Orientated languages, C++ supports inheritance. In the example below, Bar extends Foo (to steal some Java speak).

class Foo {
};
class Bar: public Foo {
};
view raw inheritance.cpp hosted with ❤ by GitHub

For classes that want members to be overridden in a Polymorphic way, those members should be declared as virtual. This means that the correct method will be determined at runtime (this does come with a slight, albeit minor, performance penalty). For classes that are intended to be abstract, there is the concept of a pure virtual method. A pure virtual method must be overridden and this is achieved by assigning the virtual method to zero. It is also convention (since C++11) to identify the overriding methods with the keyword override. The destructor can also be declared purely virtual in order to make the class abstract but then an implementation must also be provided (this is because the destructor will still be called, see below).

In the following example, Foo cannot be directly instantiated.

class Foo {
public:
// A purely virtual destructor will prevent this class being instantiated directly
virtual ~Foo() = 0;
};
class Bar: public Foo {
~Bar() override { };
};
view raw abstract.cpp hosted with ❤ by GitHub

Destructors of the concrete object and all parent classes are called when the object is deleted, starting at the child (see example at the bottom).

If objects are stored in a container, their destructors are called automatically when that container goes out of scope. However due to the way memory is allocated for the objects, storing a parent type will cause that object to be sliced at that level. Continuing with Foo and Bar, storing a Bar in a container of type Foo would result in all Bars being stored as Foos.

The way to solve this is to store Foo pointers in the container instead, this however means that destructors are not automatically called when the container goes out of scope. This will result in the memory allocated to those objects not being freed up (i.e. a memory leak). Two options to avoid this are to either explicitly call destructors on each of the objects or to use a special pointer such as uinque_ptr or shared_ptr

A unique_ptr will call the destructors and free the memory allocated to it’s object when it leaves scope, a shared_ptr keeps a count of references to that object and will call the destructors when the last reference is removed. A shared pointer is useful for example when returning the object or storing references in multiple places. Storing an object in a unique or shared pointer effectively results in a rudimentary garbage collector pattern.

The following is an example of all of the above and the output.

#include <utility>
#include <memory>
#include <iostream>
#include <vector>
using namespace std;
// Abstract class that declares a virtual destructor
class Engine {
public:
virtual ~Engine() = 0;
string get_name() { return name; };
protected:
string name;
};
Engine::~Engine() {
cout << "Engine (" << name << ") destructed" << endl;
}
class M123 : public Engine {
public:
explicit M123(string name) {
this->name = move(name);
}
~M123() override { cout << name << " destructed (M123)" << endl; }
};
class M456 : public Engine {
public:
explicit M456(string name) {
this->name = move(name);
}
~M456() override { cout << name << " destructed (M456)" << endl; }
};
void do_something() {
cout << ">> The following vector (v1) needs explicit deletion" << endl;
auto v1 = vector<Engine *>();
v1.push_back(new M123("e123"));
v1.push_back(new M456("e456"));
for (auto &e : v1) cout << e->get_name() << endl;
for (auto e : v1) delete (e); // <- Explicit destructor calling
cout << endl << ">> The following vector (v2) will be cleaned up when the scope if left" << endl;
auto v2 = vector<std::unique_ptr<Engine>>();
v2.push_back(std::make_unique<M123>("f123"));
v2.push_back(std::make_unique<M456>("f456"));
for (auto &i : v2) cout << i->get_name() << endl;
cout << ">> ending scope" << endl;
}
int main() {
do_something();
}
view raw engine.cpp hosted with ❤ by GitHub
>> The following vector (v1) needs explicit deletion
e123
e456
e123 destructed (M123)
Engine (e123) destructed
e456 destructed (M456)
Engine (e456) destructed
>> The following vector (v2) will be cleaned up when the scope is left
f123
f456
>> ending scope
f123 destructed (M123)
Engine (f123) destructed
f456 destructed (M456)
Engine (f456) destructed