Notes on Inheritance and Polymorphism¶
Introduction¶
many object-oriented languages have a feature called inheritance
inheritance lets a class inherit the variables and methods from another class
among other things, this lets you create new classes by adding functionality to existing ones
inheritance also lets you specify methods that a class must have without saying how those methods must be implemented
this last point, in particular, turns out to be extremely useful in practice
Using Multiple Classes¶
in large programs, you often uses many different classes
for instance, imagine a program with these three classes: Point
,
Person
, and Reading
#include "cmpt_error.h"
#include <iostream>
using namespace std;
class Point {
private:
double x;
double y;
public:
// default constructor
Point() : x(0), y(0) { }
// copy constructor
Point(const Point& other) : x(other.x), y(other.y) { }
Point(double a, double b) : x(a), y(b) { }
// getters
double get_x() const { return x; }
double get_y() const { return y; }
void print() const {
cout << '(' << x << ", " << y << ')';
}
void println() const {
print();
cout << "\n";
}
}; // class Point
class Person {
string name;
int age;
public:
Person(const string& n, int a)
: name{n}, age{a}
{
if (age < 0) cmpt::error("negative age");
}
string get_name() const { return name; }
int get_age() const { return age; }
void print() const {
cout << "Name: '" << name << ", Age: " << age;
}
void println() const {
print();
cout << "\n";
}
}; // class Person
class Reading {
private:
string loc;
double temp;
public:
Reading(const string& l, double t)
: loc{l}, temp{t}
{ }
string get_loc() const { return loc; }
double get_temp() const { return temp; }
void print() const {
cout << temp << " degrees at " << loc;
}
void println() const {
print();
cout << "\n";
}
}; // class Reading
int main() {
Point a{1, 2};
a.println();
Person b{"Katja", 22};
b.println();
Reading c{"backyard", 2.4};
c.println();
}
Using Multiple Classes¶
notice that the println
method is identical in Point
, Person
,
and Reading
repeated methods are often a flag that simplification is possible
so what we can do is create a class called Printable
that lets us inherit
println
automatically
class Printable {
public:
// prints the object to cout followed by "\n"
void println() const {
print();
cout << "\n";
}
}; // class Printable
now we can re-write Point
like this (we’ll only look at Point
for the
moment — the details are the same for the other two classes)
class Point : public Printable {
private:
double x;
double y;
public:
// default constructor
Point() : x(0), y(0) { }
// copy constructor
Point(const Point& other) : x(other.x), y(other.y) { }
Point(double a, double b) : x(a), y(b) { }
// getters
double get_x() const { return x; }
double get_y() const { return y; }
void print() const {
cout << '(' << x << ", " << y << ')';
}
}; // class Point
Point
is the same as before, except now it has : public Printable
at
the top, and println
is no longer in the body
Inheritance¶
the idea here is that println
is inherited by Point
, meaning it is
automatically added to Point
but there’s a problem: this example does not compile!
more specifically, the error you’ll get is that print
is not declared
inside println
class Printable {
public:
void println() const {
print(); // <---- error on this line: print() is not defined
cout << "\n";
}
}; // class Printable
while the Point
class does indeed define print()
, that’s not soon
enough for Printable
so we need to add a print()
method to Printable
but, unlike println()
, print()
will be different for every class
so what should print()
do inside Printable
?
we don’t know!
so, C++ lets us define print
without a body like this:
class Printable {
public:
// prints the object to cout
virtual void print() const = 0;
// prints the object to cout followed by "\n"
void println() const {
print();
cout << "\n";
}
}; // class Printable
the = 0
at the end of print
means that it has no body
the keyword virtual
means that classes that inherit from Printable
can
supply a body
since there is no virtual
in front of println()
, classes that inherit
from Printable
cannot change println
— they will inherit the version
of println
defined here in Printable
Inheritance and Destructors¶
with the addition of print
to Printable
, the code now compiles and
runs!
but there’s still a serious problem
to see it, lets add a destructor to Point
class Point : public Printable {
private:
// ...
public:
// ...
~Point() {
print();
cout << " destroyed\n";
}
// ...
}; // class Point
this code works as expected:
Point a{1, 2};
a.println();
it prints this:
(1, 2)
(1, 2) destroyed
this code also works as expected:
Point* p = new Point(3, 4);
p->println();
delete p;
it prints this:
(3, 4)
(3, 4) destroyed
however, it should be noted that this code does not compile using our course makefile, because it generates a warning, and for us warnings are treated as fatal errors
even though it appears to work, the fact that we get a warning for this code is a red flag that there is a potential problem
but something different happens here:
Printable* p = new Point(3, 4);
p->println();
delete p;
do you see the difference?
p
is of type Printable*
, not Point*
— everything else is the
same
this compiles (with warnings!), and it prints this:
(3, 4)
apparently the destructor is not being called!
that’s a big problem — destructors are often vital, e.g. think of the
int_vec
class where the destructor is responsible for deleting the
internal array (which avoids a memory leak)
Inheritance and Destructors¶
to see what’s happening, lets add a destructor to Printable
class Printable {
public:
// prints the object to cout
virtual void print() const = 0;
// prints the object to cout followed by "\n"
void println() const {
print();
cout << "\n";
}
~Printable() {
cout << "Printable destructor called\n";
}
}; // class Printable
now run this code again:
Printable* p = new Point(3, 4);
p->println();
delete p;
it prints this:
Printable destructor called
(3, 4)
so the Printable
destructor is called, but not the Print
destructor
and it is pretty weird that the destructor for Printable
is called before
p->print()
?!
if you read the warning message we are ignoring, it says the we are doing something that will result in undefined behaviour
warning: deleting object of abstract class type ‘Printable’ which has
non-virtual destructor will cause undefined behaviour
[-Wdelete-non-virtual-dtor]
delete p;
Inheritance and Destructors¶
the solution to these problems is to use a virtual destructor inside
Printable
class Printable {
public:
// prints the object to cout
virtual void print() const = 0;
// prints the object to cout followed by "\n"
void println() const {
print();
cout << "\n";
}
// virtual destructor
virtual ~Printable() { }
}; // class Printable
the keyword virtual
means that classes that extend Printable
are
allowed to over-ride the default destructor from Printable
and substitute
it with their own destructor
now this code compiles without warning:
Printable* p = new Point(3, 4);
p->println();
delete p;
it prints this:
(3, 4)
(3, 4) destroyed
which is exactly what we want
Virtual Destructors¶
in this course, we’ll always include virtual
destructors in a base class
(i.e. a class that will be inherited from) so that derived classes can, if
they choose to, supply their own destructors
to help with this in g++
, we will always compile with the -Wnon-virtual-
dtor
option turned on so that g++
will usually recognize situations
where we haven’t done this
Some Terminology¶
here is the Printable
class we have settled on
class Printable {
public:
// prints the object to cout
virtual void print() const = 0;
// prints the object to cout followed by "\n"
void println() const {
print();
cout << "\n";
}
// virtual destructor
virtual ~Printable() { }
}; // class Printable
Printable
is an abstract class because not all of its methods are
implemented, i.e. the print
method has no body
= 0
indicates a method is abstract; such methods are also known as pure virtual methods- a class is considered abstract if it has one, or more, abstract methods
- it’s possible to for an abstract class to have one, or more, methods that
are not
= 0
- it’s possible to for an abstract class to have one, or more, methods that
are not
also, we call Printable
a base class, because its main use is to have
other classes derive from it
for example, the Point
class is said to derive from Printable
class Point : public Printable {
// ...
};
we say that Point
is derived from Printable
or that Point
inherits from Printable
or that Point
is a subclass of Printable
or that Point
extends Printable
or that Point
is a child of Printable
we can also say that Printable
is a superclass of Point
, or that
Printable
is a parent class of Point
the keyword public
in the class header means that all the public
methods from Printable
will also be public
when they are inherited in
Point
C++ allows so-called multiple-inheritance, where a class can extend more than one class
single-inheritance means the class has exactly one parent class
in this course, we’ll usually restrict ourselves to single-inheritance, and
always use public
inheritance
Virtual Methods¶
inside Printable
, the method print
is declared virtual
virtual methods are an important concept in object-oriented programming
basically, if you declare a method in a class virtual
then that means that
derived classes can, if they choose to, supply their implementation for that
method
if you look at Point
, Person
, and Reading
, that’s what we did —
each of those classes provides its implementation for the print
method
The Idea of Binding¶
consider a regular function that is not inside any class or struct
void hello() { // hello is bound to its body at compile-time
cout << "Hello!\n";
}
when C++ compiles this function, the name hello
gets bound to (associated
with) the function body at compile-time
when you call hello()
, the compiler knows exactly what code will be
executed
when a name is bound at compile time, that’s called static binding
The Idea of Binding¶
a virtual method, such as print
in Printable
is not necessarily
bound at compile time
i.e. it is possible that, at compile-time, there is no way for the compiler to
know exactly which print()
method is being called
for example:
Printable* p;
string s;
cin >> s;
if (s == "point") {
p = new Point{2, 3};
} else {
p = new Person{"Jill", 93};
}
p.print(); // Which print is called? Point's or Person's?
there is no way for the compiler to know exactly which print
is being
called in p.print()
— it depends on what the user types
it doesn’t know which print()
to call until run-time, i.e. when the
program actually runs
in this case, we say that print
is dynamically bound, i.e. print
is associated with its body at run-time instead of compile-time
dynamic binding is one of the key features of object-oriented programming
Things to Note¶
class Point : public Printable {
// ...
}
the keyword public
here means that all the public
methods from
Printable
will also be public
when they are inherited in Point
Point
extends only one class, i.e. Printable
C++ allows so-called multiple-inheritance, where a class can extend more than one class
- Careful: while multiple inheritance does have its uses, it is more
complicated to use especially in the case when the two (or more) classes you
are inheriting from have an implemented method with the same name — which
of the two implementations should be used in the inheriting class?
- one popular solution to this problem with multiple inheritance is to simply not allow it, e.g. this is what languages such as Java and Ruby do
- also, there is no problem with multiple inheritance if the classes you are inheriting from are abstract classes, i.e. none of the methods have implementations
single-inheritance means the class has exactly one parent class
in this course, we’ll restrict ourselves to single-inheritance, and always use
public
inheritance
Printable Objects¶
it’s impossible to make just a Printable
object
Printable p; // compiler error: print() not implemented
however, pointers of type Printable*
can be created, and they are very
useful
Pointers to Printable Objects¶
Printable* p1 = new Point{1, 2};
Printable* p2 = new Person{"Max", 2};
Printable* p3 = new Reading{"Black Rock", 41.5};
p1->println(); // (1, 2)
p2->println(); // Name: 'Max, Age: 2
p3->println(); // 41.5 degrees at Black Rock
p1
, p2
, and p3
are all of type Printable*
but the objects they point to are all different types
Pointers to Printable Objects¶
suppose p
is of type Pointer*
and points to either a Point
,
Person
, or Reading
object (we don’t know which)
what does p->print()
do?
there’s no way to know without knowing the type of the object being pointed to
we can’t tell from p->print()
alone what gets printed!
A vector of Printable Objects¶
the elements of a C++ vector
(or array) must all be of the same type
so we cannot have vector
containing a Point
, Person
, and
Reading
object because they are all different types
but we can have a vector
of Pointer*
objects …
A vector of Printable Objects¶
vector<Printable*> v = { new Point{1, 2},
new Person{"Max", 2},
new Reading{"Black Rock", 41.5}
};
for(Printable* p : v) {
p->print();
cout << "\n";
}
//... be sure to delete v's elements ...
each time p->println()
is called, a different version of print()
is
called
C++ generally cannot determine which print()
is called until run-time
Pointers to Printable Objects¶
suppose p
is of type Printable*
and pointing to an object
we have no idea exactly what the exact type of the object p
is pointing to
so we can only call methods through p
that are declared in the
Printable
class
all objects that p
points to, regardless of their exact type, are
guaranteed to have print()
and println()
so p->print()
and p->println()
are the only methods we can safely call
through p
Pointers to Printable Objects¶
Reading* r = new Reading{"Black Rock", 41.5};
r->println(); // okay
cout << r->get_temp() // okay
<< "\n";
Printable* p = r; // okay
p->println(); // okay
cout << p->get_temp() // compile-time error!
<< "\n"; !
C++ and OOP¶
C++ was the first popular mainstream language to support OOP
earlier languages used it (e.g. Simula 67, SmallTalk), but have never been as popular
most new languages support OOP
most of them have somewhat simplified syntax compared to C++
e.g. Java renames ->
to .
and supports interfaces, which are like C++
classes with all the methods virtual and not implemented (= 0
)
C++ and OOP¶
we are mainly going to use inheritance in this course in the style presented in these note, i.e. base classes of mostly virtual methods
that is, as a way specify what methods must be in an object
this is quite useful in practice
some programmers even think this is the best use of inheritance (and even that
you should never inherit implemented methods like println()
)
be aware there are other styles and uses of inheritance in C++