सामग्री पर जाएं

वर्चुअल फ़ंक्शन, vtable और पॉलीमॉर्फ़िज़्म

रनटाइम पॉलीमॉर्फ़िज़्म में गहराई से उतरें: वर्चुअल डिस्पैच अंदर से कैसे काम करता है, override और final कब इस्तेमाल करें, ऐब्स्ट्रैक्ट क्लास, और RTTI।

रनटाइम पॉलीमॉर्फ़िज़्म

पॉलीमॉर्फ़िज़्म (polymorphism) का अर्थ है “कई रूप” — एक ही इंटरफ़ेस ऑब्जेक्ट के वास्तविक टाइप के अनुसार अलग-अलग व्यवहार करता है। C++ में रनटाइम पॉलीमॉर्फ़िज़्म वर्चुअल फ़ंक्शन से हासिल होता है। जब आप बेस पॉइंटर या रेफ़रेंस के ज़रिए वर्चुअल फ़ंक्शन कॉल करते हैं, तो कॉल रनटाइम पर वास्तविक डिराइव्ड क्लास के इम्प्लीमेंटेशन तक भेजी जाती है।

यही वह तंत्र है जो OOP को शक्तिशाली बनाता है: आप ऐसा कोड लिख सकते हैं जो बेस क्लास इंटरफ़ेस पर काम करता है, और वह किसी भी डिराइव्ड क्लास के साथ अपने आप सही ढंग से काम करता है — यहाँ तक कि उनके साथ भी जो आपके कोड के बाद लिखी गई हों।

vtable कैसे काम करती है

जब किसी क्लास में वर्चुअल फ़ंक्शन होते हैं, तो कंपाइलर एक vtable (वर्चुअल फ़ंक्शन टेबल) बनाता है — फ़ंक्शन पॉइंटर का एक ऐरे। उस क्लास के हर ऑब्जेक्ट में एक छिपा हुआ vptr (vtable का पॉइंटर) होता है। जब वर्चुअल फ़ंक्शन कॉल होता है, तो प्रोग्राम vptr के सहारे vtable तक जाता है, फ़ंक्शन पॉइंटर खोजता है और उसी के ज़रिए कॉल करता है। इसी अप्रत्यक्षता (indirection) के कारण वर्चुअल कॉल में सीधे कॉल की तुलना में थोड़ा ओवरहेड होता है।

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

पॉलीमॉर्फ़िक बेस के लिए वर्चुअल डिस्ट्रक्टर ज़रूरी हैं

अगर किसी क्लास में कोई भी वर्चुअल फ़ंक्शन है और डिराइव्ड ऑब्जेक्ट को बेस पॉइंटर के ज़रिए delete किया जा सकता है, तो उसका डिस्ट्रक्टर वर्चुअल होना चाहिए। इसके बिना, बेस पॉइंटर के ज़रिए डिराइव्ड ऑब्जेक्ट को delete करना अपरिभाषित व्यवहार (undefined behavior) पैदा करता है — डिराइव्ड डिस्ट्रक्टर कभी चलता ही नहीं, और रिसोर्स लीक हो जाते हैं। (जिस क्लास को कभी बेस पॉइंटर से delete नहीं किया जाता, या जो final है, वह इसका अपवाद हो सकती है; फिर भी वर्चुअल फ़ंक्शन वाली क्लास में वर्चुअल डिस्ट्रक्टर रखना सबसे सुरक्षित आदत है, और कंपाइलर अक्सर इसकी चेतावनी देते हैं।)

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 पैटर्न

कम से कम एक प्योर वर्चुअल (pure virtual) फ़ंक्शन (= 0) वाली क्लास ऐब्स्ट्रैक्ट होती है — उसका ऑब्जेक्ट नहीं बनाया जा सकता। नॉन-वर्चुअल इंटरफ़ेस (NVI) पैटर्न एक public नॉन-वर्चुअल एंट्री पॉइंट देता है जो एक private वर्चुअल फ़ंक्शन को कॉल करता है, जिससे बेस क्लास प्री/पोस्ट कंडीशन को नियंत्रित कर सकती है।

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) आपको रनटाइम पर पॉलीमॉर्फ़िक ऑब्जेक्ट का वास्तविक टाइप पूछने देता है। 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 के ज़रिए रनटाइम पॉलीमॉर्फ़िज़्म संभव बनाते हैं
  • वर्चुअल फ़ंक्शन वाली जिस क्लास को बेस के रूप में इस्तेमाल किया जाता है (बेस पॉइंटर से delete होने वाली), उसमें virtual डिस्ट्रक्टर होना चाहिए
  • ओवरराइड करते समय हमेशा override इस्तेमाल करें — यह सिग्नेचर के बेमेल को कंपाइल टाइम पर पकड़ लेता है
  • प्योर वर्चुअल फ़ंक्शन (= 0) क्लास को ऐब्स्ट्रैक्ट बनाते हैं और डिराइव्ड क्लास को उन्हें लागू करने के लिए बाध्य करते हैं
  • NVI पैटर्न बेस क्लास को कस्टमाइज़ेशन पॉइंट के आसपास प्री/पोस्ट कंडीशन लागू करने देता है
  • कंस्ट्रक्टर/डिस्ट्रक्टर में वर्चुअल फ़ंक्शन कॉल करने से बचें — डिराइव्ड क्लास अभी (या अब) पूरी तरह निर्मित नहीं होती

क्विज़ — अपना ज्ञान परखें

(20 XP)

1. अगर किसी पॉलीमॉर्फ़िक बेस क्लास में वर्चुअल डिस्ट्रक्टर **न** हो तो क्या होता है?

2. `override` कीवर्ड क्या करता है?

3. कंस्ट्रक्टर से वर्चुअल फ़ंक्शन कॉल करने से क्यों बचना चाहिए?