انتقل إلى المحتوى

Virtual Functions, vtables & Polymorphism

Dive into runtime polymorphism: how virtual dispatch works under the hood, when to use override and final, abstract classes, and RTTI.

Runtime Polymorphism

Polymorphism means "many forms" — the same interface behaves differently depending on the actual type of the object. In C++, runtime polymorphism is achieved through virtual functions. When you call a virtual function through a base pointer or reference, the call is dispatched to the actual derived class's implementation at runtime.

This is the mechanism that makes OOP powerful: you can write code that operates on a base class interface, and it automatically works correctly with any derived class — even ones written after your code.

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;
}

المدمرات الافتراضية إلزامية

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 المترجم أنك تنوي تجاوز وظيفة افتراضية من الفئة الأساسية. إذا لم يكن التوقيع متطابقًا (خطأ مطبعي شائع)، فسيكتشفه المترجم. الكلمة الأساسية final تمنع تجاوز الوظيفة أو تمنع الوراثة من الفصل.

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;
}

فئات مجردة ونمط NVI

الفئة التي تحتوي على وظيفة ظاهرية خالصة واحدة على الأقل (= 0) هي مجردة — ولا يمكن إنشاء مثيل لها. يوفر نمط الواجهة غير الافتراضية (NVI) نقطة دخول عامة غير افتراضية تستدعي وظيفة افتراضية خاصة، مما يسمح للفئة الأساسية بالتحكم في الشروط المسبقة/اللاحقة.

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 (معلومات نوع وقت التشغيل) تتيح لك الاستعلام عن النوع الفعلي لكائن متعدد الأشكال في وقت التشغيل. يقوم dynamic_cast(base_ptr) بإرسال مؤشر أساسي إلى مؤشر مشتق بشكل آمن، مما يؤدي إلى إرجاعnullptrفي حالة فشل الإرسال. بالنسبة للمراجع، يقومdynamic_castالفاشل بطرحstd::bad_cast.

typeid(obj) يُرجع مرجعstd::type_infoالذي يمكن مقارنته لتحديد نوع وقت التشغيل.

استخدم RTTI بشكل مقتصد - غالبًا ما يشير الاستخدام المتكرر لـdynamic_castإلى وجود مشكلة في التصميم. تفضل الوظائف الافتراضية للسلوك الخاص بالنوع.

خطأ شائع

لا تستدعي أبدًا الوظائف الافتراضية من المُنشئ أو المدمر. أثناء الإنشاء، يكون النوع الديناميكي للكائن هو الفئة التي يتم إنشاؤها حاليًا، وليس الفئة المشتقة النهائية. ستستدعي المكالمة الافتراضية فيBase::Base()Base::method()، وليسDerived::method()- حتى لو كان الكائن الذي يتم إنشاؤه هوDerived. يعد هذا مصدرًا معروفًا للأخطاء التي قد يكون من الصعب للغاية تشخيصها.

النقاط الرئيسية
  • تعمل الوظائف الافتراضية على تمكين تعدد الأشكال في وقت التشغيل عبر vtables وvptrs
  • Any class with virtual functions must have avirtualdestructor
  • استخدم override دائمًا عند التجاوز، فهو يكتشف عدم تطابق التوقيع في وقت الترجمة
  • تعمل الدالات الافتراضية الخالصة (= 0) على إنشاء ملخص للفئة وإجبار الفئات المشتقة على تنفيذها
  • The NVI pattern lets base classes enforce pre/post conditions around customization points
  • تجنب استدعاء الوظائف الافتراضية في المنشئات/المدمرات - الفئة المشتقة لم يتم إنشاؤها بالكامل بعد (أو لم تعد).

اختبار — اختبر معرفتك

(20 نقطة خبرة)

1. ماذا يحدث إذا كانت الفئة الأساسية متعددة الأشكال لا تحتوي على أداة تدمير افتراضية؟

2. ماذا تفعل الكلمة الرئيسية`override`؟

3. Why should you avoid calling virtual functions from a constructor?