Εικονικές Συναρτήσεις, vtables & Πολυμορφισμός
Βουτήξτε στον πολυμορφισμό χρόνου εκτέλεσης: πώς λειτουργεί η εικονική αποστολή κάτω από την κουκούλα, πότε να χρησιμοποιήσετε την παράκαμψη και τις τελικές, αφηρημένες κλάσεις και το RTTI.
Πολυμορφισμός χρόνου εκτέλεσης
Πολυμορφισμός σημαίνει "πολλές μορφές" — η ίδια διεπαφή συμπεριφέρεται διαφορετικά ανάλογα με τον πραγματικό τύπο του αντικειμένου. Στο C++, ο πολυμορφισμός χρόνου εκτέλεσης επιτυγχάνεται μέσω εικονικών συναρτήσεων. Όταν καλείτε μια εικονική συνάρτηση μέσω ενός δείκτη βάσης ή μιας αναφοράς, η κλήση αποστέλλεται στην υλοποίηση της πραγματικής παραγόμενης κλάσης κατά το χρόνο εκτέλεσης.
Αυτός είναι ο μηχανισμός που κάνει το OOP ισχυρό: μπορείτε να γράψετε κώδικα που λειτουργεί σε μια διεπαφή βασικής κλάσης και λειτουργεί αυτόματα σωστά με οποιαδήποτε παράγωγη κλάση — ακόμα και με αυτές που γράφτηκαν μετά τον κώδικά σας.
Πώς λειτουργούν τα vtables
Όταν μια κλάση έχει εικονικές συναρτήσεις, ο μεταγλωττιστής δημιουργεί έναν vtable (εικονικός πίνακας συναρτήσεων) — μια σειρά από δείκτες συναρτήσεων. Κάθε αντικείμενο αυτής της κλάσης περιέχει ένα κρυφό vptr (δείκτη προς τον πίνακα vtable). Όταν καλείται μια εικονική συνάρτηση, το πρόγραμμα ακολουθεί το vptr στον πίνακα vtable, αναζητά τον δείκτη συνάρτησης και καλεί μέσω αυτού. Αυτή η έμμεση κατεύθυνση είναι γιατί οι εικονικές κλήσεις έχουν μικρό κόστος σε σύγκριση με τις άμεσες κλήσεις.
#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;
}Οι εικονικοί καταστροφείς είναι υποχρεωτικοί
Εάν μια κλάση έχει οποιαδήποτε εικονική συνάρτηση, ο καταστροφέας της πρέπει να είναι εικονικός. Χωρίς αυτό, η διαγραφή ενός παραγόμενου αντικειμένου μέσω ενός δείκτη βάσης προκαλεί απροσδιόριστη συμπεριφορά — ο παράγωγος καταστροφέας δεν εκτελείται ποτέ, διαρρέοντας πόρους.
#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;
}παράκαμψη και τελικές λέξεις-κλειδιά
Η λέξη-κλειδί override λέει στον μεταγλωττιστή ότι σκοπεύετε να παρακάμψετε μια εικονική συνάρτηση βασικής κλάσης. Εάν η υπογραφή δεν ταιριάζει (ένα συνηθισμένο τυπογραφικό λάθος), ο μεταγλωττιστής την πιάνει. Η λέξη-κλειδί final αποτρέπει την περαιτέρω παράκαμψη μιας συνάρτησης ή αποτρέπει την κληρονομικότητα από μια κλάση.
#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;
}Abstract Classes & NVI Pattern
Μια κλάση με τουλάχιστον μία καθαρή εικονική συνάρτηση (= 0) είναι αφηρημένη — δεν μπορεί να δημιουργηθεί. Το μοτίβο Μη εικονική διεπαφή (NVI) παρέχει ένα δημόσιο μη εικονικό σημείο εισόδου που καλεί μια ιδιωτική εικονική συνάρτηση, επιτρέποντας στη βασική κλάση να ελέγχει τις συνθήκες πριν/μετά.
#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 & dynamic_cast
Το RTTI (Πληροφορίες τύπου χρόνου εκτέλεσης) σάς επιτρέπει να ρωτήσετε τον πραγματικό τύπο ενός πολυμορφικού αντικειμένου κατά το χρόνο εκτέλεσης. Το dynamic_cast εκπέμπει με ασφάλεια έναν δείκτη βάσης σε έναν παράγωγο δείκτη, επιστρέφοντας τοnullptrεάν το cast αποτύχει. Για αναφορές, ένα αποτυχημένοdynamic_castρίχνει τοstd::bad_cast.
Το typeid(obj) επιστρέφει μια αναφοράstd::type_infoπου μπορεί να συγκριθεί για τον προσδιορισμό του τύπου χρόνου εκτέλεσης.
Χρησιμοποιήστε το RTTI με φειδώ — η συχνή χρήσηdynamic_castσυχνά σηματοδοτεί ένα πρόβλημα σχεδιασμού. Προτιμήστε εικονικές συναρτήσεις για συμπεριφορά τύπου.
Ποτέ μην καλείτε εικονικές συναρτήσεις από κατασκευαστή ή καταστροφέα. Κατά τη διάρκεια της κατασκευής, ο δυναμικός τύπος του αντικειμένου είναι η κλάση που κατασκευάζεται αυτήν τη στιγμή, όχι η τελική παραγόμενη κλάση. Μια εικονική κλήση στοBase::Base()θα καλέσει τοBase::method(), όχι τοDerived::method()— ακόμα κι αν το αντικείμενο που δημιουργείται είναιDerived. Αυτή είναι μια πολύ γνωστή πηγή σφαλμάτων που μπορεί να είναι εξαιρετικά δύσκολο να διαγνωστεί.
- Οι εικονικές λειτουργίες επιτρέπουν τον πολυμορφισμό χρόνου εκτέλεσης μέσω vtables και vptrs
- Οποιαδήποτε τάξη με εικονικές συναρτήσεις πρέπει να έχει καταστροφέα
virtual - Να χρησιμοποιείτε πάντα το
overrideκατά την παράκαμψη — εντοπίζει αναντιστοιχίες υπογραφής κατά τη στιγμή της μεταγλώττισης - Οι καθαρές εικονικές συναρτήσεις (
= 0) κάνουν μια κλάση αφηρημένη και αναγκάζουν τις παραγόμενες κλάσεις να τις υλοποιήσουν - Το μοτίβο NVI επιτρέπει στις βασικές κλάσεις να επιβάλλουν συνθήκες pre/post γύρω από τα σημεία προσαρμογής
- Αποφύγετε την κλήση εικονικών συναρτήσεων σε κατασκευαστές/καταστροφείς — η παραγόμενη κλάση δεν έχει ακόμη (ή δεν έχει πλέον) πλήρως κατασκευαστεί
Κουίζ — Δοκιμάστε τις γνώσεις σας
(20 XP)1. Τι συμβαίνει εάν μια πολυμορφική βασική κλάση ΔΕΝ έχει εικονικό καταστροφέα;
2. Τι κάνει η λέξη-κλειδί`override`;
3. Γιατί πρέπει να αποφύγετε την κλήση εικονικών συναρτήσεων από έναν κατασκευαστή;