Funzioni Virtuali, vtable e Polimorfismo
Approfondisci il polimorfismo a runtime: come funziona il dispatch virtuale dietro le quinte, quando usare override e final, le classi astratte e l'RTTI.
Polimorfismo a Runtime
Polimorfismo significa "molte forme" — la stessa interfaccia si comporta in modo diverso a seconda del tipo reale dell'oggetto. In C++ il polimorfismo a runtime si ottiene tramite le funzioni virtuali. Quando chiami una funzione virtuale attraverso un puntatore o un riferimento alla base, la chiamata viene indirizzata a runtime all'implementazione della classe derivata effettiva.
È questo il meccanismo che rende potente l'OOP: puoi scrivere codice che opera sull'interfaccia di una classe base, e funzionerà automaticamente in modo corretto con qualsiasi classe derivata — anche con quelle scritte dopo il tuo codice.
Come Funzionano le vtable
Quando una classe ha funzioni virtuali, il compilatore crea una vtable (tabella delle funzioni virtuali) — un array di puntatori a funzione. Ogni oggetto di quella classe contiene un vptr nascosto (un puntatore alla vtable). Quando viene chiamata una funzione virtuale, il programma segue il vptr fino alla vtable, cerca il puntatore a funzione e lo usa per la chiamata. È questa indirezione la ragione per cui le chiamate virtuali hanno un piccolo costo rispetto alle chiamate dirette.
#include <iostream>
#include <memory>
#include <vector>
class Shape {
public:
virtual ~Shape() = default;
// Pure virtual — no default implementation
virtual double area() const = 0;
// Virtual with default implementation
virtual std::string name() const { return "Shape"; }
};
class Circle : public Shape {
double radius_;
public:
explicit Circle(double r) : radius_{r} {}
double area() const override {
return 3.14159265 * radius_ * radius_;
}
std::string name() const override { return "Circle"; }
};
class Rectangle : public Shape {
double w_, h_;
public:
Rectangle(double w, double h) : w_{w}, h_{h} {}
double area() const override { return w_ * h_; }
std::string name() const override { return "Rectangle"; }
};
// Works with ANY Shape — past, present, or future
void print_info(const Shape& s) {
std::cout << s.name() << ": area = " << s.area() << '\n';
}
int main() {
Circle c{5.0};
Rectangle r{3.0, 4.0};
print_info(c); // Circle: area = 78.5398
print_info(r); // Rectangle: area = 12
// Polymorphic container
std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Circle>(2.0));
shapes.push_back(std::make_unique<Rectangle>(6.0, 3.0));
for (const auto& s : shapes) {
print_info(*s); // virtual dispatch at runtime
}
return 0;
}I Distruttori Virtuali Sono Obbligatori
Se una classe ha anche solo una funzione virtuale, il suo distruttore deve essere virtuale. Senza di esso, distruggere un oggetto derivato attraverso un puntatore alla base causa comportamento indefinito — il distruttore della derivata non viene mai eseguito, lasciando risorse non rilasciate.
#include <iostream>
#include <memory>
class Base {
public:
// Without virtual destructor: UNDEFINED BEHAVIOR on delete
// virtual ~Base() = default; // ALWAYS do this!
~Base() { std::cout << "~Base\n"; }
};
class Derived : public Base {
int* data_;
public:
Derived() : data_{new int[1000]} {
std::cout << "Derived: allocated\n";
}
~Derived() {
delete[] data_;
std::cout << "~Derived: freed\n";
}
};
int main() {
Base* ptr = new Derived{};
delete ptr; // Only ~Base runs! ~Derived is NOT called!
// Result: memory leak (data_ never freed)
// Fix: make ~Base() virtual
return 0;
}Le Parole Chiave override e final
La parola chiave override comunica al compilatore che intendi ridefinire una funzione virtuale della classe base. Se la firma non corrisponde (un refuso frequente), il compilatore lo segnala. La parola chiave final impedisce ulteriori override di una funzione, oppure impedisce di ereditare da una classe.
#include <iostream>
class Base {
public:
virtual ~Base() = default;
virtual void process(int x) const { std::cout << "Base: " << x << '\n'; }
virtual void execute() { std::cout << "Base::execute\n"; }
};
class Middle : public Base {
public:
// override: compiler checks signature matches Base::process
void process(int x) const override {
std::cout << "Middle: " << x * 2 << '\n';
}
// final: no further derived class can override execute()
void execute() final { std::cout << "Middle::execute\n"; }
};
class Bottom : public Middle {
public:
void process(int x) const override {
std::cout << "Bottom: " << x * 3 << '\n';
}
// ERROR if uncommented: execute() is final in Middle
// void execute() override { }
};
// A final class cannot be inherited from at all
class Leaf final : public Base {
public:
void process(int x) const override {
std::cout << "Leaf: " << x << '\n';
}
};
// ERROR if uncommented: cannot inherit from final class
// class Attempt : public Leaf {};
int main() {
Bottom b;
Base& ref = b;
ref.process(10); // Bottom: 30 (virtual dispatch)
ref.execute(); // Middle::execute (final version)
return 0;
}Classi Astratte e Pattern NVI
Una classe con almeno una funzione virtuale pura (= 0) è astratta — non può essere istanziata. Il pattern Non-Virtual Interface (NVI) fornisce un punto di ingresso pubblico non virtuale che chiama una funzione virtuale privata, permettendo alla classe base di controllare pre-condizioni e post-condizioni.
#include <iostream>
#include <string>
// NVI Pattern: public non-virtual calls private virtual
class Validator {
public:
virtual ~Validator() = default;
// Public non-virtual interface — controls the workflow
bool validate(const std::string& input) const {
if (input.empty()) {
std::cout << "[Validator] Input is empty\n";
return false;
}
// Delegate to derived class implementation
bool result = do_validate(input);
std::cout << "[Validator] " << (result ? "PASS" : "FAIL") << '\n';
return result;
}
private:
// Pure virtual — derived classes MUST implement
virtual bool do_validate(const std::string& input) const = 0;
};
class EmailValidator : public Validator {
bool do_validate(const std::string& input) const override {
return input.find('@') != std::string::npos
&& input.find('.') != std::string::npos;
}
};
class LengthValidator : public Validator {
std::size_t min_len_;
bool do_validate(const std::string& input) const override {
return input.length() >= min_len_;
}
public:
explicit LengthValidator(std::size_t min) : min_len_{min} {}
};
int main() {
EmailValidator email_v;
LengthValidator len_v{5};
email_v.validate("user@example.com"); // PASS
email_v.validate("invalid"); // FAIL
len_v.validate("hi"); // FAIL
len_v.validate("hello!"); // PASS
return 0;
}RTTI e dynamic_cast
L'RTTI (Run-Time Type Information) ti permette di interrogare a runtime il tipo reale di un oggetto polimorfico. dynamic_cast converte in modo sicuro un puntatore alla base in un puntatore alla derivata, restituendo nullptr se la conversione fallisce. Per i riferimenti, un dynamic_cast fallito lancia std::bad_cast.
typeid(obj) restituisce un riferimento a std::type_info che può essere confrontato per identificare il tipo a runtime.
Usa l'RTTI con parsimonia — un uso frequente di dynamic_cast segnala spesso un problema di progettazione. Per i comportamenti specifici di un tipo, preferisci le funzioni virtuali.
Non chiamare mai funzioni virtuali da un costruttore o da un distruttore. Durante la costruzione, il tipo dinamico dell'oggetto è la classe che si sta costruendo in quel momento, non la classe derivata finale. Una chiamata virtuale in Base::Base() chiamerà Base::method(), non Derived::method() — anche se l'oggetto che si sta creando è un Derived. Questa è una nota fonte di bug che possono essere estremamente difficili da diagnosticare.
- Le funzioni virtuali abilitano il polimorfismo a runtime tramite vtable e vptr
- Ogni classe con funzioni virtuali deve avere un distruttore
virtual - Usa sempre
overridequando ridefinisci — individua le firme non corrispondenti a tempo di compilazione - Le funzioni virtuali pure (
= 0) rendono astratta una classe e obbligano le derivate a implementarle - Il pattern NVI permette alle classi base di far rispettare pre-condizioni e post-condizioni attorno ai punti di personalizzazione
- Evita di chiamare funzioni virtuali in costruttori e distruttori — la classe derivata non è ancora (o non è più) completamente costruita
Quiz — Metti alla prova le tue conoscenze
(20 XP)1. Cosa succede se una classe base polimorfica NON ha un distruttore virtuale?
2. Cosa fa la parola chiave `override`?
3. Perché dovresti evitare di chiamare funzioni virtuali da un costruttore?