Skip to content
academia.sh

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* or Shape& 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, this always 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& parameter 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.

Start typing to search.

↑↓ ↵ Esc navigate · open · close