Lesson 09 / 14
Virtual Functions and Vtables
The measurement of whether a virtual call through a base pointer or reference always works with the same object, independent of the expression's static type.
Published Updated
In this lesson
- Shows that a virtual call selects which function to run based on the object's real type
- Measures that a call through a base pointer or reference always works with the same object
- Verifies that each base pointer in an array reaches the object it itself points to, not another object
Contents
The previous lesson measured that the base subobject is a part of the derived
object, its address coinciding with it — but no function was virtual, and the
function to be called was determined at compile time by looking at the expression’s
static type. This lesson adds the virtual keyword: now a call through a Shape*
or Shape& decides which function to run not by the pointer’s static type, but by
the real type of the object it points to. The question has changed: it is not
the base subobject’s address, but which object a virtual call really works with
that is measured.
A Virtual Call Reaches the Same Object
Rectangle redefines Shape’s introduce function. When object r is accessed
both through a Shape* pointer and a Shape& reference and introduce is called,
does both reach r’s own function?
// virtual_same_object.cpp -- virtual call reaches the same object independent of static type
#include <iostream>
class Shape {
public:
virtual ~Shape() = default;
virtual void introduce(const void* expected) const {
std::cout << "introduce(Shape): running object same as expected "
<< std::boolalpha << (static_cast<const void*>(this) == expected) << "\n";
}
};
class Rectangle : public Shape {
public:
void introduce(const void* expected) const override {
std::cout << "introduce(Rectangle): running object same as expected "
<< std::boolalpha << (static_cast<const void*>(this) == expected) << "\n";
}
};
int main() {
Rectangle r;
Shape* base_ptr = &r;
Shape& base_ref = r;
base_ptr->introduce(&r);
base_ref.introduce(&r);
return 0;
}
introduce(Rectangle): running object same as expected true introduce(Rectangle): running object same as expected true
PS4. Both calls were made through an expression whose static type is Shape
(base_ptr, base_ref), but both printed “introduce(Rectangle)” — the virtual
call selected Rectangle::introduce by looking not at the pointer’s/reference’s
static type, but at the real type of the object it points to. Inside the function,
comparing this to r‘s address returns true: the call not only selects the
right function, it also runs that selected function exactly on r itself, not on a
copy or another object. (M08/K02 asked which type resolved the call; a similar
mechanism exists here too, but what is measured is which object the call works
with — and while Java has no object slicing, assigning to a base type by value in
C++ would construct a new and incomplete object; here there is only pointer/
reference access, slicing does not come into play.) The manually built method
table (a struct carrying function pointers) in M11/K03’s Object-Like Design lesson
corresponds here to the compiler-generated virtual function table; the mechanism’s
details were covered there and are not re-explained here. The class is self:
this always carries r’s address, independent of the static type of the
expression through which the call was made.
An Array of Base Pointers: Each Call Reaches Its Own Object
When a Shape* array points to different derived objects, does each element reach
the object it itself points to, or is there mixing between them?
// virtual_array.cpp -- each base pointer in the array reaches the object it itself points to
#include <iostream>
class Shape {
public:
virtual ~Shape() = default;
virtual bool same_as(const void* address) const {
return static_cast<const void*>(this) == address;
}
};
class Rectangle : public Shape {};
class Triangle : public Shape {};
int main() {
Rectangle r;
Triangle t;
Shape* shapes[2] = { &r, &t };
std::cout << std::boolalpha;
std::cout << "shapes[0] same as its own object r "
<< shapes[0]->same_as(&r) << "\n";
std::cout << "shapes[1] same as its own object t "
<< shapes[1]->same_as(&t) << "\n";
std::cout << "shapes[0] same as t (cross-check) "
<< shapes[0]->same_as(&t) << "\n";
return 0;
}
shapes[0] same as its own object r true shapes[1] same as its own object t true shapes[0] same as t (cross-check) false
PS5. Rectangle and Triangle do not redefine introduce, they inherit
Shape::same_as as is — but the function still binds this to the real object of
the element it was called through, every time it is called. shapes[0]->same_as(&r)
is true, shapes[1]->same_as(&t) is true: each element reaches its own
object. The cross-check makes this even clearer — shapes[0] (the element pointing
to r) compared against &t returns false, because the object shapes[0]
works with is r, not t. The class is self: every base pointer in the array,
even though it carries the same base type, works with no object other than the
particular object it itself points to.
Passing to a Function by Reference Does Not Change the Object
An object passed to a function with a const Shape& parameter — inside the
function, is it still the same object as outside, or the function’s own copy?
// virtual_reference_parameter.cpp -- object passed by reference to a function is the same as the one outside
#include <iostream>
class Shape {
public:
virtual ~Shape() = default;
virtual bool same_as(const void* address) const {
return static_cast<const void*>(this) == address;
}
};
class Rectangle : public Shape {};
void verify(const Shape& s, const void* expected) {
std::cout << std::boolalpha
<< "object called inside the function same as expected "
<< s.same_as(expected) << "\n";
}
int main() {
Rectangle r;
verify(r, &r);
return 0;
}
object called inside the function same as expected true
PS6. In the call verify(r, &r), s is not a copy of r, it is a reference
that binds to r itself — the const Shape& parameter type provides this without
constructing a copy. When s.same_as(&r) is called inside the function, the virtual
call again selects Shape::same_as (because Rectangle did not redefine it), and
comparing this to the outside r‘s address returns true. Crossing the
function boundary — as long as the parameter is a reference — does not change the
object’s identity: s inside verify is the same object as r inside main. The
class is self: as long as the function call does not construct a copy, a
virtual call always works with the real object at the point the call started.
override and final: Still Reaching the Same Object by Real Type
BorderedTriangle derives from Triangle, marks same_as as both override and
final, and is itself final. These two keywords affect compile-time checking, not
run-time behavior — but does the virtual call that ends up running still reach the
same object?
// virtual_override_final.cpp -- virtual call marked override and final still runs based on the object's real type
#include <iostream>
class Shape {
public:
virtual ~Shape() = default;
virtual bool same_as(const void* address) const {
return static_cast<const void*>(this) == address;
}
};
class Triangle : public Shape {
public:
bool same_as(const void* address) const override {
return static_cast<const void*>(this) == address;
}
};
class BorderedTriangle final : public Triangle {
public:
bool same_as(const void* address) const override final {
return static_cast<const void*>(this) == address;
}
};
int main() {
BorderedTriangle b;
Shape& base_ref = b;
std::cout << std::boolalpha
<< "same_as called through the base reference same as expected "
<< base_ref.same_as(&b) << "\n";
return 0;
}
same_as called through the base reference same as expected true
PS23. override tells the compiler “this function redefines a base class
function,” and if the signature does not match (for example if const is
forgotten, or a parameter type is written wrong) it produces a compile error — this
prevents a mismatched signature from silently defining a new, separate function (and
the base function never being redefined). This protection operates at compile time;
since the sandbox only runs code that compiles successfully, how a signature
mismatch leads to an error cannot be shown by running it here, only narrated.
final prevents both class BorderedTriangle and function same_as from being
further derived from/redefined, at compile time — this too is something observable
not by running it, but only by a forbidden violation failing to compile. The part
that runs has not changed: base_ref.same_as(&b) prints true, because
override/final do not change run-time behavior, they only narrow which
signatures are considered valid. The class is self: a final class’s object,
like any other, is exactly the object the virtual call reaches.
Virtual Destructor: Deleting Through a Base Pointer Runs the Correct Chain
Shape’s destructor is virtual. When a Rectangle constructed with new is
deleted through a Shape*, does destruction start from Rectangle’s own
destructor, or jump straight to Shape’s?
// virtual_destructor_delete_through_base_pointer.cpp -- virtual destructor runs the correct destruction chain when deleted through a base pointer
#include <iostream>
class Shape {
public:
virtual ~Shape() { std::cout << "Shape destructor\n"; }
};
class Rectangle : public Shape {
public:
~Rectangle() override { std::cout << "Rectangle destructor\n"; }
};
int main() {
Shape* base_ptr = new Rectangle();
std::cout << "-- deletion begins --\n";
delete base_ptr;
return 0;
}
-- deletion begins -- Rectangle destructor Shape destructor
PS24. base_ptr‘s static type is Shape*, but when delete base_ptr is
called, Rectangle destructor is printed first, then Shape destructor —
destruction, like construction, runs from most derived to most base, and what lets
it start in the correct order is ~Shape() being virtual. This is the behavior
the standard defines: when the base class’s destructor is virtual, a delete
through a base pointer starts by calling the real (dynamic) type’s destructor. The
counterpart has no defined outcome: if ~Shape() were not virtual, this
delete base_ptr, where the static and dynamic types differ, would be undefined
behavior per the standard. On common compilers this usually shows up as only
Shape’s destructor running and the resources Rectangle added not being
released, but that is a compiler behavior, not a language guarantee. This branch is
not run here: since undefined behavior does not give a deterministic output, it has
no result verifiable in the sandbox, it can only be narrated. The class is self:
the delete call always runs the full destruction chain of r (the real object),
independent of base_ptr’s static type.
A Manually Built Method Table and the Compiler’s Virtual Function Table Give the Same Result
M11/K03’s Object-Like Design lesson manually built a “method table” using a
struct that carried function pointers. Does a call through such a manually built
table and a virtual call through the compiler-generated virtual function table
give the same result for the same object?
// virtual_manual_method_table.cpp -- a manually built method table gives the same result as the compiler-generated virtual function table
#include <iostream>
struct Shape {
virtual ~Shape() = default;
virtual int area() const { return 0; }
};
struct Square : public Shape {
explicit Square(int side) : side_(side) {}
int area() const override { return side_ * side_; }
int side_;
};
struct ManualMethodTable {
int (*area)(const void*);
};
int square_area_manual(const void* object) {
return static_cast<const Square*>(object)->area();
}
int main() {
Square square(5);
Shape& virtual_access = square;
ManualMethodTable table{ square_area_manual };
std::cout << "compiler's virtual call: " << virtual_access.area() << "\n";
std::cout << "manually built table call: " << table.area(&square) << "\n";
std::cout << std::boolalpha << "both results the same "
<< (virtual_access.area() == table.area(&square)) << "\n";
return 0;
}
compiler's virtual call: 25 manually built table call: 25 both results the same true
PS25. virtual_access.area() reaches Square::area through the
compiler-generated virtual function table; table.area(&square) reaches the same
function (this time directly through a function pointer) through the manually built
ManualMethodTable. Both print 25 and the comparison is true: two different
mechanisms select the same function for the same object. The difference is in how
the selection is made — in a virtual call the compiler adds a hidden virtual
function table pointer to every object, and area() finds the right entry through
that pointer every time it is called; in a manually built table this step is the
responsibility of the programmer here in the ManualMethodTable table{...} line and
matching the table to the correct object. This mechanism was covered in detail in
M11/K03; here only the fact that the two approaches arrive at the same result is
verified. The class is self: both calls work on square itself.
The Structural Cost of Virtual Dynamic Dispatch: A Larger Object
VirtualPoint carries the same two int fields as NonVirtualPoint, with one
difference: it has a virtual destructor. Are these two classes, carrying the same
fields, the same size, or does carrying a virtual function have a per-object
structural cost?
// virtual_dynamic_dispatch_cost.cpp -- a class carrying a virtual function is larger than a non-virtual class carrying the same fields
#include <iostream>
class NonVirtualPoint {
public:
int x_ = 0;
int y_ = 0;
};
class VirtualPoint {
public:
virtual ~VirtualPoint() = default;
int x_ = 0;
int y_ = 0;
};
int main() {
std::cout << std::boolalpha
<< "is the class carrying a virtual function larger than the one that does not "
<< (sizeof(VirtualPoint) > sizeof(NonVirtualPoint)) << "\n";
return 0;
}
is the class carrying a virtual function larger than the one that does not true
PS26. The two classes carry exactly the same two int fields, but the
comparison is true: VirtualPoint is larger. The difference comes from the
hidden virtual function table pointer the compiler adds to every object as soon as
a virtual function is defined — this is dynamic dispatch’s per-object,
one-time structural cost. Besides this there is also a per-call cost: every
virtual call performs an indirection that first finds the right entry through the
object’s virtual function table pointer, instead of a direct jump. This second cost
is not measured here, because a timing measurement would depend on hardware,
compiler optimization level, and the sandbox’s own load — this lesson does not
report such environment-dependent data. What is measurable and deterministic is
only the size difference. The class is part: the virtual function table
pointer is a part of the object, not a separate object.
Virtual Call Inside the Constructor: Goes to the Base, Not the Derived
Base’s constructor calls define() inside its own body; Derived redefines this
function. When a Derived object is constructed, does this call inside Base’s
constructor reach Derived::define, or Base::define?
// virtual_call_inside_constructor.cpp -- virtual call inside a constructor goes to the base's own function, not the derived's
#include <iostream>
class Base {
public:
Base() {
std::cout << "calling define() inside Base constructor\n";
define();
}
virtual void define() const {
std::cout << "Base::define ran\n";
}
};
class Derived : public Base {
public:
Derived() {
std::cout << "calling define() inside Derived constructor\n";
define();
}
void define() const override {
std::cout << "Derived::define ran\n";
}
};
int main() {
Derived d;
return 0;
}
calling define() inside Base constructor Base::define ran calling define() inside Derived constructor Derived::define ran
PS27. d’s final form is a Derived, but while Base’s constructor is
running, the define() call inside it runs Base::define, not
Derived::define. Why: while Base’s constructor is running, Derived‘s own part
has not been constructed yet — the object’s effective type at that moment, from the
perspective of virtual dispatch, is still Base. This changes as the chain
proceeds upward: while Derived‘s own constructor is running, the define() call
made inside it now runs Derived::define, because at that point the object’s
Derived part has also been constructed. The class is self: both calls work on
d itself, but which function is selected depends on where the construction chain
stands at the moment the call is made.
The Bounding Measurement
The virtual calls measured in this lesson were always made through a pointer or a
reference; assigning Rectangle to a Shape variable by value
(Shape s = r;) is outside this measurement’s scope — such an assignment does not
engage the virtual call, it engages the slicing touched on at the end of the
previous lesson, and the result is a new, incomplete Shape object. The
measurement also covers only the simple cases where introduce and same_as do
not themselves make another virtual call inside them; the incomplete destruction
that could arise if a base destructor were not virtual was never observed in this
lesson, because both classes (Shape and its derivatives) carry a virtual
destructor. A base class built entirely from functions with no body at all, only
presenting an interface — abstract classes — is not this lesson’s subject; the
next lesson measures that.
Summary
- A virtual call through a
Shape*orShape&decides which function to run not by the expression’s static type, but by the real type of the object it points to. - Inside the selected function,
thisalways carries the address of the same real object, independent of the base-type expression the call was made through — the class is self. - Each element in a
Shape*array reaches the particular object it itself points to; a cross-check confirms it does not match another element’s object. - An object passed to a function with a
const Shape¶meter is the same object outside as it is inside the function — reference binding does not construct a copy. - Assigning by value to the base type (slicing) is outside this lesson’s scope; a virtual call works this way only under pointer/reference access.
Next Step
In this lesson Shape::introduce and Shape::same_as had a body — they could be
called even though derived classes did not redefine them. The next lesson, Abstract
Classes and Interfaces, measures the case where a base class is built entirely from
functions with no body at all, declaring only a contract — there, an object cannot
be constructed directly from the base class, only from derived classes; what is
measured is again which object a virtual call works with, but this time the base
type has no instance of its own at all.
To keep your progress and take notes, Log in
My notes
Log in to take notes.