Virtual functions
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:
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}
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:
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}
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:
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}
copy is a Notification object: only the base part of e was copied into it (slicing, last lesson), so it is a notification through and through. ref refers to e itself, which is an Email, so the virtual call finds Email::send.
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:
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}
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:
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}
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.
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}
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.
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}
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:
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}
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.
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.
rectangle: 6 square: 16 square: 1 Total: 23
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}
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.
Hello, Ana Ben, your bill is due
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}
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.
Downloading photo.jpg Downloading song.mp3 Deleted temp copy of song.mp3 Closed song.mp3 Deleted temp copy of photo.jpg Closed photo.jpg
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}
What you learned
- Without
virtual, a call through a base reference or pointer runs the base's version, chosen when compiling. - With
virtualin 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
overrideon every replacing function: if the name, parameters orconstdon't match, you getmarked 'override', but does not overrideinstead 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.