Virtual functions and polymorphism
A virtual function lets a base class declare what a derived class may change, and lets a call through a base reference reach the derived version. The choice happens at run time from the actual type of the object, not from the type of the pointer.
That is dynamic polymorphism, and it is the reason a container of base pointers can hold different shapes and call area() on each. The lesson covers the keyword, the two safety features that go with it, and the small run-time cost that comes with the mechanism.
Dispatch through a base reference
The base class declares virtual int area() const and gives it a body. Each derived class overrides it, and a call through const shape& reaches the version of the object that was passed, not the version of the reference.
The program passes a rectangle and a triangle to the same function. Without virtual, both calls would run shape::area; with it, the output has two different numbers.
The object itself does not change and no copy is made: a reference to the base subobject is enough, because the vtable pointer lives in the object and the call is resolved from there.
#include <iostream>
class shape
{
public:
virtual ~shape() = default;
virtual int area() const
{
return 0;
}
};
class rectangle : public shape
{
public:
rectangle(int width, int height) : width_(width), height_(height) {}
int area() const override
{
return width_ * height_;
}
private:
int width_;
int height_;
};
class triangle : public shape
{
public:
triangle(int base, int height) : base_(base), height_(height) {}
int area() const override
{
return base_ * height_ / 2;
}
private:
int base_;
int height_;
};
void print_area(const shape& s)
{
std::cout << s.area() << '\n';
}
int main()
{
rectangle r{4, 5};
triangle t{6, 3};
print_area(r);
print_area(t);
}
g++ -std=c++23 -Wall -Wextra virtual.cpp -o virtual_demo
./virtual_demo
20
9
override
The override keyword after a function declaration tells the compiler that this function is meant to replace a virtual one from the base. If the signature does not match exactly, the build stops instead of quietly creating a new function.
The program's derived class writes int area() without const, while the base declares int area() const. Without override that is a different function and the program prints 0; with override the compiler reports that it does not override anything.
Use override on every virtual function you write. It costs nothing at run time and turns a silent bug into a message that names the function and the base.
#include <iostream>
class shape
{
public:
virtual ~shape() = default;
virtual int area() const
{
return 0;
}
};
class square : public shape
{
public:
explicit square(int side) : side_(side) {}
int area() override
{
return side_ * side_;
}
private:
int side_;
};
int main()
{
square s{4};
std::cout << s.area() << '\n';
}
g++ -std=c++23 -Wall -Wextra override.cpp -o override_demo
override.cpp:19:9: error: 'int square::area()' marked 'override', but does not override
19 | int area() override
| ^~~~
override.cpp:8:17: warning: 'virtual int shape::area() const' was hidden [-Woverloaded-virtual=]
8 | virtual int area() const
| ^~~~
override.cpp:19:9: note: by 'int square::area()'
19 | int area() override
| ^~~~
The virtual destructor
Deleting a derived object through a base pointer runs the destructor the pointer's type knows, unless the destructor is virtual. A non-virtual base destructor leaves the derived part of the object undestroyed, which is undefined behaviour.
The program stores a derived object behind a base pointer, deletes it through that pointer, and prints a line from each destructor. Because ~shape is virtual, both run, derived first and base second.
Make the destructor of every polymorphic base virtual, even when it has nothing to release. virtual ~shape() = default; is the one line that makes deletion through the interface safe.
#include <iostream>
class base
{
public:
virtual ~base()
{
std::cout << "destroy base\n";
}
};
class derived : public base
{
public:
~derived() override
{
std::cout << "destroy derived\n";
}
};
int main()
{
base* p = new derived;
delete p;
}
g++ -std=c++23 -Wall -Wextra destructor.cpp -o destructor
./destructor
destroy derived
destroy base
final
final on a virtual function forbids further overriding; final on a class forbids deriving from it. Both are promises to the compiler and to the reader, and both are enforced at compile time.
The program below derives from a class whose area() is final and tries to override it. The compiler reports that the function is final and cannot be overridden.
Use final when a class is not designed for inheritance or when a function must keep its behaviour. It also lets the compiler devirtualise calls in some cases, because it knows no other implementation can exist.
class shape
{
public:
virtual ~shape() = default;
virtual int area() const final
{
return 0;
}
};
class square : public shape
{
public:
int area() const override
{
return 4;
}
};
int main()
{
square s;
return s.area();
}
g++ -std=c++23 -Wall -Wextra final.cpp -o final_demo
final.cpp:15:9: error: virtual function 'virtual int square::area() const' overriding final function
15 | int area() const override
| ^~~~
final.cpp:6:17: note: overridden function is 'virtual int shape::area() const'
6 | virtual int area() const final
| ^~~~
What virtual costs
A class with a virtual function carries a pointer to its vtable, so every object grows by one pointer and every virtual call reads that pointer before it jumps. The program prints the size of a class with and without a virtual function.
On this 64-bit build the plain class is 4 bytes and the polymorphic one is 16: eight bytes of vptr plus four of data, padded to the alignment of the pointer. The call itself is an indirect jump, which the processor predicts well in a loop with one type.
The cost is small and the design benefit is large, but it is not zero: a virtual call cannot be inlined unless the compiler can prove the type, and an object that is copied by value loses the dispatch, as lesson 034 showed.
#include <iostream>
class plain
{
public:
int value() const
{
return value_;
}
private:
int value_ = 0;
};
class polymorphic
{
public:
virtual ~polymorphic() = default;
virtual int value() const
{
return value_;
}
private:
int value_ = 0;
};
int main()
{
std::cout << sizeof(plain) << ' ' << sizeof(polymorphic) << '\n';
}
g++ -std=c++23 -Wall -Wextra cost.cpp -o cost
./cost
4 16
Exercises
- Remove virtual from shape::area in virtual.cpp and rerun. What does print_area print for both objects?
- Add a third shape with its own area() and pass it to print_area. Which line of main changes?
- Mark square final in override.cpp, remove override from area(), and see whether the program compiles. What does area() return now?
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What does a virtual function allow?
Answer
A call through a base reference to reach the derived version
The call is resolved at run time from the dynamic type of the object.
-
Why does a polymorphic base need a virtual destructor?
Answer
A delete through the base must run the derived destructor
The static type at the delete decides the destructor, and virtual dispatch reaches the right one.
-
What does
overridecatch?Answer
A function that does not actually override a base function
The keyword makes a signature mismatch a compile error at the class.
-
What does
finalprevent?Answer
Further overriding of the function or the class
final closes the override chain, and a further attempt is an error.
-
What does this program print?
#include <iostream> struct shape { virtual ~shape() = default; virtual int area() const = 0; }; struct square : shape { int area() const override { return 20; } }; struct triangle : shape { int area() const override { return 9; } }; void show(const shape& s) { std::cout << s.area() << ' '; } int main() { square a; triangle b; show(a); show(b); }Answer
20 9
Each call through the base reference reaches the override of the actual object.
-
Why does this code fail to compile?
struct shape { virtual int area() const { return 0; } }; struct square : shape { int area() override { return 4; } };Answer
'int square::area()' marked 'override', but does not override
The derived signature leaves out const, so it does not override the base function and the keyword makes that an error.
-
What does this program print?
#include <iostream> struct base { virtual ~base() { std::cout << "destroy base\n"; } }; struct derived : base { ~derived() { std::cout << "destroy derived\n"; } }; int main() { base* p = new derived; delete p; }Answer
destroy derived / destroy base
The delete through the base runs both destructors, derived first.
-
What does overriding a
finalfunction report?Answer
virtual function 'square::area()' overriding final function
The base function is marked final, and the compiler rejects the override.
-
What do the two sizes print?
#include <iostream> struct base { virtual ~base() = default; }; struct derived : base { int value = 0; }; int main() { std::cout << sizeof(base) << ' ' << sizeof(derived); }Answer
8 16
The virtual table pointer makes the base eight bytes, and the derived object adds an int with padding to sixteen.
-
Why store base pointers in a container?
Answer
One container can hold different derived objects and call their virtual functions
The pointer keeps the dynamic type, so the virtual call reaches the right override.
The code on this page was built with g++ 15.2.0 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container. The programs whose section shows a compiler error were compiled until that error and were not run.
Article author: Arthur Isaev