Programming Language & Its Applications β Virtual Functions and File Handling, NEC licence examination syllabus (Nepal Engineering Council).
One call site, many behaviours β decided at runtime by the object's actual type, through a table of function pointers.
read() on a file descriptor, it does not know whether the target is an SSD, a network socket or a keyboard β a table of function pointers decides at runtime. That is exactly the vtable you are about to see, and Linux implements it by hand in C with struct file_operations. C++ just gave the pattern a keyword. Search "Linux file_operations struct" after this page and you will recognise a hand-rolled vtable immediately β it is the same idea, written out longhand.#include <iostream>
class Animal {
public:
void eat() { std::cout << " Animal eats\n"; }
virtual void speak() { std::cout << " ...\n"; }
virtual ~Animal() = default;
};
class Dog : public Animal {
public:
void eat() { std::cout << " Dog eats bones\n"; }
void speak() override { std::cout << " Woof!\n"; }
};
class Cat : public Animal {
public:
void eat() { std::cout << " Cat eats fish\n"; }
void speak() override { std::cout << " Meow!\n"; }
};
int main() {
Dog d; Cat c;
std::cout << "through the object (static both ways):\n";
d.eat(); d.speak();
std::cout << "through Animal* :\n";
Animal *pets[] = { &d, &c };
for (Animal *a : pets) {
a->eat(); /* NOT virtual -> always Animal::eat */
a->speak(); /* virtual -> the real type wins */
}
std::cout << "through Animal& :\n";
Animal &ref = d;
ref.speak(); /* references dispatch too */
std::cout << "by VALUE (sliced):\n";
Animal sliced = d;
sliced.speak(); /* Animal::speak β d's Dog-ness is gone */
return 0;
}
Output:
through the object (static both ways):
Dog eats bones
Woof!
through Animal* :
Animal eats
Woof!
Animal eats
Meow!
through Animal& :
Woof!
by VALUE (sliced):
...
#include <iostream>
class NoVirtual { int x; };
class OneVirtual { int x; public: virtual void f() {} };
class ManyVirtual { int x; public: virtual void a() {}
virtual void b() {}
virtual void c() {}
virtual void d() {} };
class DerivedFromOne : public OneVirtual { int y; };
int main() {
std::cout << "NoVirtual = " << sizeof(NoVirtual) << " (int only)\n";
std::cout << "OneVirtual = " << sizeof(OneVirtual) << " (int + vptr + pad)\n";
std::cout << "ManyVirtual = " << sizeof(ManyVirtual)<< " (STILL one vptr)\n";
std::cout << "DerivedFromOne = " << sizeof(DerivedFromOne)
<< " (vptr shared, not duplicated)\n";
return 0;
}
Output:
NoVirtual = 4 (int only)
OneVirtual = 16 (int + vptr + pad)
ManyVirtual = 16 (STILL one vptr)
DerivedFromOne = 16 (vptr shared, not duplicated)
std::vector, std::string and the other standard containers have no virtual functions at all. For a type meant to be stored by the million, an 8-byte vptr per element is unacceptable. Polymorphism is a tool with a price tag, not a default.
#include <iostream>
#include <memory>
#include <vector>
#include <cmath>
class Shape {
protected:
std::string name;
public:
Shape(std::string n) : name(std::move(n)) {}
virtual double area() const = 0; /* pure */
virtual double perimeter() const = 0; /* pure */
virtual ~Shape() = default;
/* a NON-virtual function using virtual ones β
the "template method" pattern */
void report() const {
std::cout << " " << name
<< ": area=" << area()
<< " perim=" << perimeter()
<< " ratio=" << area()/perimeter() << "\n";
}
};
class Circle : public Shape {
double r;
public:
explicit Circle(double rad) : Shape("Circle "), r(rad) {}
double area() const override { return M_PI*r*r; }
double perimeter() const override { return 2*M_PI*r; }
};
class Rectangle : public Shape {
double w, h;
public:
Rectangle(double a, double b)
: Shape("Rectangle"), w(a), h(b) {}
double area() const override { return w*h; }
double perimeter() const override { return 2*(w+h); }
};
class Triangle : public Shape {
double a, b, c;
public:
Triangle(double x, double y, double z)
: Shape("Triangle "), a(x), b(y), c(z) {}
double perimeter() const override { return a+b+c; }
double area() const override {
double s = perimeter()/2;
return std::sqrt(s*(s-a)*(s-b)*(s-c));
}
};
int main() {
/* Shape s("x"); β abstract, cannot instantiate */
std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Circle>(3));
shapes.push_back(std::make_unique<Rectangle>(4, 6));
shapes.push_back(std::make_unique<Triangle>(3, 4, 5));
double total = 0;
for (const auto &s : shapes) { s->report(); total += s->area(); }
std::cout << " total area = " << total << "\n";
return 0;
}
Output:
Circle : area=28.2743 perim=18.8496 ratio=1.5
Rectangle: area=24 perim=20 ratio=1.2
Triangle : area=6 perim=12 ratio=0.5
total area = 58.2743
#include <iostream>
class Base {
public:
virtual void f(int) { std::cout << " Base::f(int)\n"; }
virtual void g() const { std::cout << " Base::g() const\n"; }
virtual void h() { std::cout << " Base::h()\n"; }
virtual ~Base() = default;
};
class Silent : public Base {
public:
/* ALL THREE are typos that compile as NEW functions,
silently hiding rather than overriding: */
void f(double) { std::cout << " Silent::f(double)\n"; }
void g() { std::cout << " Silent::g()\n"; }
void H() { std::cout << " Silent::H()\n"; }
};
class Safe : public Base {
public:
void f(int) override { std::cout << " Safe::f(int)\n"; }
void g() const override { std::cout << " Safe::g() const\n"; }
void h() final { std::cout << " Safe::h() [final]\n"; }
/* void f(double) override; β error: does not override
void g() override; β error: const mismatch
void H() override; β error: no such base fn */
};
/* class Deeper : public Safe { void h() override; };
β error: virtual function 'h' overrides a final function */
int main() {
Silent s; Safe f;
Base *p1 = &s, *p2 = &f;
std::cout << "Silent (typos compiled fine):\n";
p1->f(7); p1->g(); p1->h();
std::cout << "Safe (override enforced):\n";
p2->f(7); p2->g(); p2->h();
return 0;
}
Output:
Silent (typos compiled fine):
Base::f(int)
Base::g() const
Base::h()
Safe (override enforced):
Safe::f(int)
Safe::g() const
Safe::h() [final]
std::variant plus std::visit β which gets the same flexibility with zero indirection. Game engines and audio code do this routinely. Search "CRTP static polymorphism" and "std::variant vs virtual functions"; comparing the three approaches is one of the better ways to understand what virtual dispatch actually buys you.Create a free account to tick topics off, take notes as you read, watch the video lessons and get a day-by-day study plan built around your exam date.
Loadingβ¦