رفتن به محتوا

توابع مجازی، vtableها و چندریختی

به چندریختی زمان اجرا شیرجه بزنید: نحوه کار ارسال مجازی در پشت صحنه، زمان استفاده از override و final، کلاس‌های انتزاعی و RTTI.

چندریختی زمان اجرا

چندریختی (Polymorphism) به معنای «اشکال متعدد» است — یک رابط مشترک بسته به نوع واقعی شیء رفتار متفاوتی نشان می‌دهد. در ‎C++‎، چندریختی زمان اجرا از طریق توابع مجازی (Virtual Functions) حاصل می‌شود. وقتی یک تابع مجازی را از طریق اشاره‌گر یا ارجاع کلاس پایه فراخوانی می‌کنید، فراخوانی در زمان اجرا به پیاده‌سازی کلاس مشتق‌شده واقعی ارسال می‌شود.

این مکانیزمی است که OOP را قدرتمند می‌کند: می‌توانید کدی بنویسید که با رابط کلاس پایه کار می‌کند و به‌صورت خودکار با هر کلاس مشتق‌شده — حتی آن‌هایی که پس از کد شما نوشته شده‌اند — به‌درستی کار می‌کند.

نحوه کار vtableها

وقتی یک کلاس توابع مجازی دارد، کامپایلر یک vtable (جدول توابع مجازی) ایجاد می‌کند — آرایه‌ای از اشاره‌گرهای تابع. هر شیء از آن کلاس شامل یک vptr مخفی (اشاره‌گر به vtable) است. وقتی یک تابع مجازی فراخوانی می‌شود، برنامه vptr را دنبال کرده، اشاره‌گر تابع را در vtable جستجو کرده و از طریق آن فراخوانی می‌کند. این غیرمستقیم بودن دلیل سربار اندک فراخوانی‌های مجازی نسبت به فراخوانی‌های مستقیم است.

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_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 از بازنویسی بیشتر یک تابع یا از ارث‌بری از یک کلاس جلوگیری می‌کند.

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) انتزاعی است — نمی‌توان نمونه‌ای از آن ساخت. الگوی رابط غیرمجازی (Non-Virtual Interface یا 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 باشد. این یک منبع شناخته‌شده باگ است که تشخیص آن می‌تواند بسیار دشوار باشد.

نکات کلیدی
  • توابع مجازی چندریختی زمان اجرا را از طریق vtableها و vptrها فعال می‌کنند
  • هر کلاسی با توابع مجازی باید یک مخرب virtual داشته باشد
  • همیشه هنگام بازنویسی از override استفاده کنید — عدم تطابق امضا را در زمان کامپایل شناسایی می‌کند
  • توابع مجازی محض (= 0) کلاس را انتزاعی می‌کنند و کلاس‌های مشتق‌شده را مجبور به پیاده‌سازی آن‌ها می‌کنند
  • الگوی NVI به کلاس‌های پایه اجازه اعمال پیش/پس‌شرط‌ها حول نقاط سفارشی‌سازی را می‌دهد
  • از فراخوانی توابع مجازی در سازنده‌ها/مخرب‌ها اجتناب کنید — کلاس مشتق‌شده هنوز (یا دیگر) کاملاً ساخته نشده است

آزمون — دانش خود را بسنجید

(20 امتیاز)

1. اگر یک کلاس پایه چندریختی مخرب مجازی نداشته باشد، چه اتفاقی می‌افتد؟

2. کلمه کلیدی `override` چه کاری انجام می‌دهد؟

3. چرا باید از فراخوانی توابع مجازی از سازنده اجتناب کنید؟