If you are not familiar with Inheritance, I recommend you read my short article about inheritance first.

One of the features of Object Oriented Programming is Polymorphism. Virtual functions in C++ allow developers to achieve run-time polymorphism by overwriting methods of a base class.

In my article about inheritance, I showed how we can create classes that inherit from a base (or parent) class. Something similar to the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Greeter {
 public:
  void talk() {
    std::cout << "hello" << std::endl;
  }
};

class SpanishGreeter : public Greeter {
 public:
  void talk() {
    std::cout << "hola" << std::endl;
  }
};

Polymorphism comes into play when a method receives an object as an argument, and we want this object to behave differently depending on what it is. This is easier to understand with an example.

Let’s say we have a program that greets people in their native tongue. The program doesn’t want to know how to greet in different languages. Instead, it uses a Greeter that knows how to correctly greet the user:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <iostream>

class Greeter {
 public:
  void talk() {
    std::cout << "hello" << std::endl;
  }
};

class SpanishGreeter : public Greeter {
 public:
  void talk() {
    std::cout << "hola" << std::endl;
  }
};

class Program {
 public:
  Program(Greeter g) : greeter_(g) {};

  void run() {
    greeter_.talk();
  }

 private:
  Greeter greeter_;
};

int main() {
  Greeter englishGreeter;
  Program p(englishGreeter);
  p.run();
}

The example above works as expected, it will print hello.

If we wanted to use a different Greeter, we might be surprised that the output is also hello:

1
2
3
4
5
6
7
...

int main() {
  SpanishGreeter spanishGreeter;
  Program p(spanishGreeter);
  p.run();
}

The reason the output is hello is because Programs’s constructor expects a Greeter not a SpanishGreeter. This causes the SpanishGreeter to be converted to a Greeter, and calling the talk method on a Greeter prints hello.

The solution to this is to achieve run-time polymorphism on the talk method of Greeter. This means that we want children of Greeter to be able to define their own behavior for the talk method.

Virtual

Here is where the virtual keyword comes to the rescue. Defining a method as virtual in Greeter tells the compiler that this method is overwritable by children classes.

The virtual keyword gets us almost there, but to achieve run-time polymorphism, we need to make greeter a pointer or a reference. If we didn’t do this, we would be creating a new Greeter on the constructor and the link to the original class would be lost:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <iostream>

class Greeter {
 public:
  // Add `virtual` here
  virtual void talk() {
    std::cout << "hello" << std::endl;
  }
};

class SpanishGreeter : public Greeter {
 public:
  void talk() {
    std::cout << "hola" << std::endl;
  }
};

class Program {
 public:
  Program(Greeter* g) : greeter_(g) {};

  void run() {
    greeter_->talk();
  }

 private:
  // Make `greeter_` a pointer
  Greeter* greeter_;
};

int main() {
  SpanishGreeter spanishGreeter;
  Program p(&spanishGreeter);
  p.run();
}

The program above print hola as expected.

Abstract classes

There are times when we want to declare an interface for a class (methods for a class), but we don’t want to define the methods right away. Instead, we want children classes to define this behavior. Classes that contain methods that are not defined are called Abstract classes.

For the Greeter example above, I decided to use English as the default language, but I could instead make Greeter an Abstract class and leave it to the children to implement the talk method. Undefined methods in an Abstract class are called pure virtual methods:

1
2
3
4
class Greeter {
 public:
  virtual void talk() = 0;
};

Trying to instantiate an abstract class, will result in a compiler error, because some methods are not defined. Abstract classes can still be used as parameters. This means we can rewrite the code above like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <iostream>

class Greeter {
 public:
  virtual void talk() = 0;
};

class SpanishGreeter : public Greeter {
 public:
  void talk() {
    std::cout << "hola" << std::endl;
  }
};

class Program {
 public:
  Program(Greeter* g) : greeter_(g) {};

  void run() {
    greeter_->talk();
  }

 private:
  Greeter* greeter_;
};

int main() {
  SpanishGreeter spanishGreeter;
  Program p(&spanishGreeter);
  p.run();
}

The only difference is that Greeter.talk() is now a pure virtual method, which makes Greeter an Abstract class.

[ programming  c++  ]
Configuring ESP32 to act as Access Point
Aligned and packed data in C and C++
ESP32 Non-Volatile Storage (NVS)
Making HTTP / HTTPS requests with ESP32
Modularizing ESP32 Software