Sari la conținut

Funcții virtuale, vtable și polimorfism

Pătrundeți-vă în polimorfismul runtime: cum funcționează dispeceratul virtual sub capotă, când să utilizați clasele de suprascriere și finale, abstracte și RTTI.

Polimorfismul de rulare

Polimorfism înseamnă „multe forme” — aceeași interfață se comportă diferit în funcție de tipul real al obiectului. În C++, polimorfismul de rulare se realizează prin funcții virtuale. Când apelați o funcție virtuală printr-un indicator sau o referință de bază, apelul este trimis către implementarea clasei derivate efective în timpul execuției.

Acesta este mecanismul care face OOP puternic: puteți scrie cod care funcționează pe o interfață de clasă de bază și funcționează automat corect cu orice clasă derivată - chiar și cu cele scrise după codul dvs.

How vtables Work

When a class has virtual functions, the compiler creates a vtable (virtual function table) — an array of function pointers. Each object of that class contains a hidden vptr (pointer to the vtable). When a virtual function is called, the program follows the vptr to the vtable, looks up the function pointer, and calls through it. This indirection is why virtual calls have a small overhead compared to direct calls.

polymorphism.cpp
#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;
}

Virtual Destructors Are Mandatory

If a class has any virtual function, its destructor must be virtual. Without it, deleting a derived object through a base pointer causes undefined behavior — the derived destructor never runs, leaking resources.

virtual_destructor.cpp
#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 and final Keywords

The override keyword tells the compiler you intend to override a base class virtual function. If the signature doesn't match (a common typo), the compiler catches it. The final keyword prevents further overriding of a function or prevents inheritance from a class.

override_final.cpp
#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

A class with at least one pure virtual function (= 0) is abstract — it cannot be instantiated. The Non-Virtual Interface (NVI) pattern provides a public non-virtual entry point that calls a private virtual function, letting the base class control pre/post conditions.

nvi_pattern.cpp
#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 (Run-Time Type Information) lets you query the actual type of a polymorphic object at runtime. dynamic_cast(base_ptr) safely casts a base pointer to a derived pointer, returningnullptrif the cast fails. For references, a faileddynamic_castthrowsstd::bad_cast.

typeid(obj) returns astd::type_inforeference that can be compared to identify the runtime type.

Use RTTI sparingly — frequentdynamic_castusage often signals a design problem. Prefer virtual functions for type-specific behavior.

Capcană

Never call virtual functions from a constructor or destructor. During construction, the object's dynamic type is the class currently being constructed, not the final derived class. A virtual call inBase::Base()will callBase::method(), notDerived::method()— even if the object being created is aDerived. This is a well-known source of bugs that can be extremely difficult to diagnose.

Idei cheie
  • Funcțiile virtuale permit polimorfismul runtime prin vtables și vptrs
  • Orice clasă cu funcții virtuale trebuie să aibă un destructorvirtual
  • Folosiți întotdeauna override când suprascrieți - observă nepotriviri ale semnăturilor la momentul compilării
  • Funcțiile pur virtuale (= 0) fac o clasă abstractă și forțează clasele derivate să le implementeze
  • Modelul NVI permite claselor de bază să impună condiții pre/post în jurul punctelor de personalizare
  • Evitați apelarea funcțiilor virtuale în constructori/destructori - clasa derivată nu este încă (sau nu mai este) complet construită

Test — Verifică-ți cunoștințele

(20 XP)

1. Ce se întâmplă dacă o clasă de bază polimorfă NU are un destructor virtual?

2. Ce face cuvântul cheie`override`?

3. De ce ar trebui să evitați apelarea funcțiilor virtuale de la un constructor?