Here's an example I wish I'd seen earlier when learning about inheritance in C++.
Let's start with a simple base class.
class Base {
public:
void method() {
speak("base method\n");
}
};
Here speak
is just a wrapper around cout
and endl
.
void speak(const string out) {
cout << out << endl;
}
Let's now inherit from Base
and re-use the same method name.
class Derived : public Base {
public:
void method() {
speak("derived method\n");
}
};
So: base classes print "base", derived classes print "derived". However, what happens if we mix types? Concretely, what would the output of the following program be?
#include <iostream>
using std::cout;
using std::endl;
using std::string;
// convenience function to print
void speak(const string out) {
cout << out << endl;
}
class Base {
public:
void method() {
speak("base method\n");
}
};
class Derived : public Base {
public:
void method() {
speak("derived method\n");
}
};
int main() {
speak("ex1: base class");
Base base = Base();
base.method();
speak("ex2: derived class");
Derived derived = Derived();
derived.method();
speak("ex3: base class ref");
Base& derived_ref = derived;
derived_ref.method();
// Derived& base_ref_error = base;
// error: non-const lvalue reference to type 'Derived' cannot bind to a
// value of unrelated type 'Base'
}
Examples 1 and 2 are straightforward. What about example 3? Intuitively, we expect derived_ref.method()
to print out "derived". It is a reference to a Derived
object. However, this is the output:
ex1: base class
base method
ex2: derived class
derived method
ex3: base class ref
base method
Shock! The reason is that, in example 3, derived_ref
is of type Base&
. The name is misleading: it's really a base_ref
:). When it looks for method
it looks it up in the Base
class.
When dealing with inheritance this situation is more common than one would like, especially when dealing with abstract classes / interfaces.
As an aside: the last example Derived& base_ref_error = base;
would not compile if uncommented.
runtime polymorphism
So, how do we fix example 3 above? The solution is to mark the base class method as virtual
.
#include <iostream>
using std::cout;
using std::endl;
using std::string;
// convenience function to print
void speak(const string out) {
cout << out << endl;
}
class Base {
public:
virtual void method() {
speak("base method\n");
}
};
class Derived : public Base {
public:
void method() override {
speak("derived method\n");
}
};
int main() {
speak("ex1: base class");
Base base = Base();
base.method();
speak("ex2: derived class");
Derived derived = Derived();
derived.method();
speak("ex3: base class ref");
Base& derived_ref = derived;
derived_ref.method();
}
This time the output is:
ex1: base class
base method
ex2: derived class
derived method
ex3: base class ref
derived method
Great! This makes more sense. The variable derived_ref
is, after all, built from derived
, which is of type Derived
.
Note: the override
keyword in the Derived
class is unnecessary. However, it's useful to have it there to help the compiler find bugs.
abstract classes
A typical scenario where the virtual
stuff comes up is with abstract classes. Here is an example:
class AbstractBase {
public:
virtual void method() = 0;
virtual ~AbstractBase() = default;
};
We have modified base to have method
be unimplemented. The =0
means we require any derived class to implement that method.
We also mark the destructor as virtual
, as subtle bugs can arise if we don't. See below.
#include <iostream>
using std::cout;
using std::endl;
using std::string;
// convenience function to print
void speak(const string out) {
cout << out << endl;
}
class AbstractBase {
public:
virtual void method() = 0;
virtual ~AbstractBase() = default;
};
class Derived : public AbstractBase {
public:
void method() override {
speak("derived method\n");
}
};
int main() {
// AbstractBase abstractbase = AbstractBase();
// error: allocating an object of abstract class type 'AbstractBase''Base'
speak("ex2: derived class");
Derived derived = Derived();
derived.method();
speak("ex3: base class ref");
AbstractBase& derived_ref = derived;
derived_ref.method();
}
The output is
ex2: derived class
derived method
ex3: base class ref
derived method
as expected.
As an aside: the first example AbstractBase abstractbase = AbstractBase();
does not compile, as AbstractBase
is, indeed, abstract.
destructors
Let's clarify what happens if we don't have a destructor marked as virtual
. Here is a first example.
#include <iostream>
using std::cout;
using std::endl;
using std::string;
// convenience function to print
void speak(const string out) {
cout << out << endl;
}
class Base {
public:
~Base() {
speak("base destruct");
}
};
class Derived : public Base {
public:
~Derived() {
speak("derived destruct");
}
};
int main() {
Derived der;
}
If we run this, the output is
derived destruct
base destruct
as the destructors for der
are invoked in order. However, tweaking this slightly, we get some unexpected behavior (I got this example from "C++ Crash Course" by Josh Lospinoso).
#include <iostream>
using std::cout;
using std::endl;
using std::string;
// convenience function to print
void speak(const string out) {
cout << out << endl;
}
class Base {
public:
~Base() {
speak("base destruct");
}
};
class Derived : public Base {
public:
~Derived() {
speak("derived destruct");
}
};
int main() {
Base* x{new Derived()};
delete x;
}
The output is
base destruct
so ~Derived
is never invoked! This could lead to serious issues. The simple fix is to have the destructor be virtual
.
#include <iostream>
using std::cout;
using std::endl;
using std::string;
// convenience function to print
void speak(const string out) {
cout << out << endl;
}
class Base {
public:
virtual ~Base() {
speak("base destruct");
}
};
class Derived : public Base {
public:
~Derived() {
speak("derived destruct");
}
};
int main() {
Base* x{new Derived()};
delete x;
}
so now the output is
derived destruct
base destruct
as expected.
Top comments (0)