Kodewick
Lesson 24 of 30Contents

Virtual functions

Lesson 24 of 30 · Part 6 · Inheritance and templates · about 20 minutes

Last lesson left a problem: through a base reference or pointer, you get the base's function, even when the object is a derived one that redefined it. Here it is again, with notifications. deliver takes any Notification, and gets an email and a text message:

main.cpp
1#include <iostream>2 3class Notification {4public:5    void send() const {6        std::cout << "Sending a notification\n";7    }8};9 10class Email : public Notification {11public:12    void send() const {13        std::cout << "Sending an email\n";14    }15};16 17class TextMessage : public Notification {18public:19    void send() const {20        std::cout << "Sending a text message\n";21    }22};23 24void deliver(const Notification& n) {25    n.send();26}27 28int main() {29    Email e;30    TextMessage t;31    deliver(e);32    deliver(t);33    return 0;34}
Output
Sending a notification
Sending a notification

Both lines say "notification". When the compiler turns n.send() into code, all it knows is that n is a Notification&, so it calls Notification::send once and for all. Which object will be behind n is only known when the program runs.

virtual

Put **virtual in front of the function in the base, and the choice is put off until the program runs: each call looks at the object itself and runs its class's version. Over in the derived classes, write override** after the parentheses:

main.cpp
1#include <iostream>2 3class Notification {4public:5    virtual void send() const {6        std::cout << "Sending a notification\n";7    }8};9 10class Email : public Notification {11public:12    void send() const override {13        std::cout << "Sending an email\n";14    }15};16 17class TextMessage : public Notification {18public:19    void send() const override {20        std::cout << "Sending a text message\n";21    }22};23 24void deliver(const Notification& n) {25    n.send();26}27 28int main() {29    Email e;30    TextMessage t;31    deliver(e);32    deliver(t);33    return 0;34}
Output
Sending an email
Sending a text message

The only change is one virtual and two overrides, and deliver now does the right thing for each. Press Step: from n.send() in deliver, the first time the program goes into Email::send, the second time into TextMessage::send. One line of code, a different function each time, depending on the object: this is called polymorphism.

A function that's virtual in the base stays virtual in every derived class, with or without the word. To do this, every object of a class with virtual functions carries a hidden pointer to its class's list of versions; the Step view doesn't show it.

Virtual calls only happen through a reference or a pointer. A copy is just a base object:

Guess first

What does this show?

1#include <iostream>2 3class Notification {4public:5    virtual void send() const {6        std::cout << "notification\n";7    }8};9 10class Email : public Notification {11public:12    void send() const override {13        std::cout << "email\n";14    }15};16 17int main() {18    Email e;19    Notification copy = e;20    Notification& ref = e;21    copy.send();22    ref.send();23    return 0;24}

override

override isn't required, but always write it. A derived function only replaces a virtual one when everything matches: name, parameters, and the const too. Forget the const and it's a different function, and override catches it:

main.cpp
1#include <iostream>2 3class Notification {4public:5    virtual void send() const {6        std::cout << "Sending a notification\n";7    }8};9 10class Email : public Notification {11public:12    void send() override {13        std::cout << "Sending an email\n";14    }15};16 17int main() {18    Email e;19    const Notification& n = e;20    n.send();21    return 0;22}
Output
main.cpp:12:10: error: 'void Email::send()' marked 'override', but does not override
   12 |     void send() override {
      |          ^~~~

'void Email::send()' marked 'override', but does not override: there's no send() without const in the base to replace. Look at the first error: line and read it as "this doesn't match anything in the base". Add the const and it compiles.

Without override, the same mistake compiles quietly: Email::send becomes a new function that only hides the base's, and calls through n still go to Notification::send. That kind of bug is hard to find; override turns it into a compile error.

Many shapes in one vector

Polymorphism pays off when you have many objects of different classes. Put them in one vector as pointers to the base, and one loop handles them all. std::unique_ptr (lesson 21) deletes them when the vector dies:

main.cpp
1#include <iostream>2#include <memory>3#include <string>4#include <vector>5 6class Shape {7public:8    virtual ~Shape() {}9 10    virtual double area() const {11        return 0;12    }13 14    virtual std::string name() const {15        return "shape";16    }17};18 19class Rectangle : public Shape {20public:21    Rectangle(double w, double h) : w_(w), h_(h) {}22 23    double area() const override {24        return w_ * h_;25    }26 27    std::string name() const override {28        return "rectangle";29    }30 31private:32    double w_;33    double h_;34};35 36class Circle : public Shape {37public:38    Circle(double r) : r_(r) {}39 40    double area() const override {41        return 3.14 * r_ * r_;42    }43 44    std::string name() const override {45        return "circle";46    }47 48private:49    double r_;50};51 52int main() {53    std::vector<std::unique_ptr<Shape>> shapes;54    shapes.push_back(std::make_unique<Rectangle>(3, 4));55    shapes.push_back(std::make_unique<Circle>(1));56    shapes.push_back(std::make_unique<Rectangle>(2, 5));57    double total = 0;58    for (const auto& s : shapes) {59        std::cout << s->name() << ": " << s->area() << '\n';60        total += s->area();61    }62    std::cout << "Total: " << total << '\n';63    return 0;64}
Output
rectangle: 12
circle: 3.14
rectangle: 10
Total: 25.14

std::make_unique<Rectangle>(3, 4) makes a Rectangle, and it goes into the vector as a unique_ptr<Shape>, just as a Rectangle* can become a Shape*. The loop only knows about Shape; add a Triangle class tomorrow and the loop stays as it is.

Why pointers and not std::vector<Shape>? A Shape element only has room for a Shape, so a rectangle put in would be sliced into a plain shape. The virtual ~Shape() {} on the first line of the class is explained at the end of this lesson.

Pure virtual functions and abstract classes

What's the area of a plain "shape"? There's no sensible answer; the return 0; above was made up. Write = 0 instead of a body, and the function becomes pure virtual: "every derived class must write this". A class with a pure virtual function is an abstract class: it exists to be a base, and you can't create an object of it.

main.cpp
1#include <iostream>2 3class Shape {4public:5    virtual ~Shape() {}6    virtual double area() const = 0;7};8 9class Square : public Shape {10public:11    Square(double side) : side_(side) {}12 13    double area() const override {14        return side_ * side_;15    }16 17private:18    double side_;19};20 21int main() {22    Square sq{3};23    std::cout << sq.area() << '\n';24    Shape s;25    return 0;26}
Output
main.cpp: In function 'int main()':
main.cpp:24:11: error: cannot declare variable 's' to be of abstract type 'Shape'
   24 |     Shape s;
      |           ^
main.cpp:3:7: note:   because the following virtual functions are pure within 'Shape':
    3 | class Shape {
      |       ^~~~~
main.cpp:6:20: note:     'virtual double Shape::area() const'
    6 |     virtual double area() const = 0;
      |                    ^~~~

cannot declare variable 's' to be of abstract type 'Shape'. Square sq{3}; on the line before is fine: it wrote area. A derived class that forgets to write it is still abstract, and creating one gives the same error with its own name in it. So the compiler reminds you of every function a new shape needs. Pointers and references to an abstract class are fine; that's how the vector above holds its shapes.

Virtual destructors

When a derived object is deleted through a base pointer, which destructor runs? Without virtual, the same thing as before: only the base's.

main.cpp
1#include <iostream>2 3class Notification {4public:5    ~Notification() {6        std::cout << "Notification cleaned up\n";7    }8 9    virtual void send() const = 0;10};11 12class Email : public Notification {13public:14    ~Email() {15        std::cout << "Email draft deleted\n";16    }17 18    void send() const override {19        std::cout << "Sending an email\n";20    }21};22 23int main() {24    Notification* n = new Email;25    n->send();26    delete n;27    return 0;28}
Output
Sending an email
Notification cleaned up

"Email draft deleted" never shows: delete n only knew n is a Notification*. If Email had memory or a file to give back, it would be left behind. (By the rules of C++ this is undefined behavior; g++ does what you see here. Compile with g++ -Wall and it warns on the delete n; line: [-Wdelete-non-virtual-dtor].) Make the base's destructor virtual:

main.cpp
1#include <iostream>2 3class Notification {4public:5    virtual ~Notification() {6        std::cout << "Notification cleaned up\n";7    }8 9    virtual void send() const = 0;10};11 12class Email : public Notification {13public:14    ~Email() {15        std::cout << "Email draft deleted\n";16    }17 18    void send() const override {19        std::cout << "Sending an email\n";20    }21};22 23int main() {24    Notification* n = new Email;25    n->send();26    delete n;27    return 0;28}
Output
Sending an email
Email draft deleted
Notification cleaned up

Now delete n finds the object's own destructor, ~Email, and after it ~Notification runs as usual: derived first, then base, as last lesson. Press Step in both programs and compare where the program goes after delete n;.

The same goes for std::unique_ptr<Notification> and the vector of shapes: they delete through a base pointer too. The rule is short: a class with virtual functions gets a virtual destructor, even an empty one like virtual ~Shape() {}.

Practice

Write the code, then press Check. A hint costs nothing; the answer is there when you need it.

Exercise 1

Add a square

Write a class Square derived from Shape: its constructor takes the side, area returns side times side, and name returns "square". The loop in main stays as it is. The program should show the lines below.

It should print
rectangle: 6
square: 16
square: 1
Total: 23
exercise1.cpp
1#include <iostream>2#include <memory>3#include <string>4#include <vector>5 6class Shape {7public:8    virtual ~Shape() {}9    virtual int area() const = 0;10    virtual std::string name() const = 0;11};12 13class Rectangle : public Shape {14public:15    Rectangle(int w, int h) : w_(w), h_(h) {}16 17    int area() const override {18        return w_ * h_;19    }20 21    std::string name() const override {22        return "rectangle";23    }24 25private:26    int w_;27    int h_;28};29 30int main() {31    std::vector<std::unique_ptr<Shape>> shapes;32    shapes.push_back(std::make_unique<Rectangle>(2, 3));33    shapes.push_back(std::make_unique<Square>(4));34    shapes.push_back(std::make_unique<Square>(1));35    int total = 0;36    for (const auto& s : shapes) {37        std::cout << s->name() << ": " << s->area() << '\n';38        total += s->area();39    }40    std::cout << "Total: " << total << '\n';41    return 0;42}
Output
Press Run or Ctrl+Enter
Exercise 2

Fix it: an override that misses

This doesn't compile. Read the first error:, then fix Reminder so it really replaces Message's function. The program should show the two lines below.

It should print
Hello, Ana
Ben, your bill is due
exercise2.cpp
1#include <iostream>2#include <string>3 4class Message {5public:6    virtual ~Message() {}7 8    virtual std::string text(const std::string& name) const {9        return "Hello, " + name;10    }11};12 13class Reminder : public Message {14public:15    std::string text(std::string name) override {16        return name + ", your bill is due";17    }18};19 20void show(const Message& m, const std::string& name) {21    std::cout << m.text(name) << '\n';22}23 24int main() {25    Message hello;26    Reminder bill;27    show(hello, "Ana");28    show(bill, "Ben");29    return 0;30}
Output
Press Run or Ctrl+Enter
Exercise 3

Fix it: the temp files stay behind

Each TempDownload should delete its temp copy when it's deleted, but those lines never show. Change only the Download class (leave TempDownload and main alone) so the output matches.

It should print
Downloading photo.jpg
Downloading song.mp3
Deleted temp copy of song.mp3
Closed song.mp3
Deleted temp copy of photo.jpg
Closed photo.jpg
exercise3.cpp
1#include <iostream>2#include <memory>3#include <string>4 5class Download {6public:7    Download(const std::string& file) : file_(file) {}8 9    ~Download() {10        std::cout << "Closed " << file_ << '\n';11    }12 13    virtual void start() const = 0;14 15protected:16    std::string file_;17};18 19class TempDownload : public Download {20public:21    TempDownload(const std::string& file) : Download(file) {}22 23    ~TempDownload() {24        std::cout << "Deleted temp copy of " << file_ << '\n';25    }26 27    void start() const override {28        std::cout << "Downloading " << file_ << '\n';29    }30};31 32int main() {33    std::unique_ptr<Download> a = std::make_unique<TempDownload>("photo.jpg");34    std::unique_ptr<Download> b = std::make_unique<TempDownload>("song.mp3");35    a->start();36    b->start();37    return 0;38}
Output
Press Run or Ctrl+Enter

What you learned

  • Without virtual, a call through a base reference or pointer runs the base's version, chosen when compiling.
  • With virtual in the base, the call looks at the object when the program runs and runs its class's version: polymorphism. It works through references and pointers, not through copies.
  • Write override on every replacing function: if the name, parameters or const don't match, you get marked 'override', but does not override instead of a quiet bug.
  • std::vector<std::unique_ptr<Shape>> holds different shapes, and one loop handles them all.
  • virtual double area() const = 0; is pure virtual; a class with one is abstract and can't be created, and derived classes must write the function.
  • A class with virtual functions needs a virtual destructor; without one, deleting through a base pointer skips the derived destructor.