سلسلة ++c للمحترفين الدرس 24: التوابع الوهمية (Virtual Member Functions) في Cpp


محمد الميداوي

الدوال الوهمية النهائية (Final virtual functions)

قدّمت C++‎ 11 المُحدِّد ‎final‎ الذي يمنع إعادة تعريف (overriding) تابع في حال ظهر في بصمته (signature):

class Base {
    public:
        virtual void foo() {
            std::cout << "Base::Foo\n";
        }
};
class Derived1: public Base {
    public:
        // Base::foo تخطي
       void foo() final {
            std::cout << "Derived1::Foo\n";
        }
};
class Derived2: public Derived1 {
    public:
        // Compilation error: cannot override final method
        virtual void foo() {
            std::cout << "Derived2::Foo\n";
        }
};

لا يمكن استخدام المُحدّد ‎final‎ إلا مع دالة تابعة وهمية (virtual)، ولا يمكن تطبيقه على الدوال التابعة غير الوهمية. وكما في حال ‎final‎، فهناك أيضًا مُحدِّدٌ يُسمّى override، وهو يمنع تخطي الدوالّ الوهمية في الصنف المشتق. كذلك يمكن دمج المُحدِّدين ‎override‎ و ‎final‎ معًا على النحو التالي:

class Derived1: public Base {
    public: void foo() final override {
        std::cout << "Derived1::Foo\n";
    }
};

استخدام override و virtual معًا في C++‎ 11 والإصدارات الأحدث

يكون للمحدّد ‎override‎ معنى خاصًّا في الإصدار C++‎ 11 وما بعده عندما يُلحَق بنهاية بصمة الدالّة، فهو يشير إلى أن الدّالة:

  • تتخطى الدالّةَ الحالية في الصنف الأساسي (base class)، وأنّ …
  • دالّة الصنف الأساسي وهمية ‎virtual‎.

الهدف الأساسي من هذا المُحدّد هو توجيه المُصرّف، يوضّح المثال أدناه التغيّر في السلوك في حال استخدام ‎override‎ وفي حال عدم استخدامها:

عند عدم استخدام ‎override‎

#include <iostream>

struct X {
    virtual void f() {
        std::cout << "X::f()\n";
    }
};

()Y::f لن تتخطى ()x::f لأن لها بصمة مختلفة، لكن سيقبل المصرِّفُ الشيفرة ويتجاهل ()Y::f بصمت. انظر:

struct Y: X {
     virtual void f(int a) {
        std::cout << a << "\n";
    }
};

مع ‎override‎:

#include <iostream>

struct X {
    virtual void f() {
        std::cout << "X::f()\n";
    }
};

سينبهك المصرِّف إلى حقيقة أن ()Y::f لا تتخطى شيئًا.

struct Y: X {
    virtual void f(int a) override {
        std::cout << a << "\n";
    }
};

لاحظ أنّ ‎override‎ ليست كلمة مفتاحية، بل مُعرِّف خاص لا يظهر إلا في بصمات الدوالّ، ويمكن استخدام ذلك المعرِّف في جميع السياقات الأخرى:

void foo() {
    int override = 1; // حسنا
    int virtual = 2; // Compilation error: keywords can't be used as identifiers.
}

الدوال التابعة الوهمية وغير الوهمية

مع الدوال التابعة الوهمية:

#include <iostream>

struct X {
    virtual void f() {
        std::cout << "X::f()\n";
    }
};

سيكون تحديد virtual هنا اختياريًا لأنها يمكن أن تُستنتج من ()X::f، انظر:

struct Y: X {
    virtual void f() {
        std::cout << "Y::f()\n";
    }
};
void call(X & a) {
    a.f();
}
int main() {
    X x;
    Y y;
    call(x); // يكون خرجها: "X::f()"
    call(y); // يكون خرجها: "Y::f()"
}

بدون الدوال التابعة الوهمية:

#include <iostream>

struct X {
    void f() {
        std::cout << "X::f()\n";
    }
};
struct Y: X {
    void f() {
        std::cout << "Y::f()\n";
    }
};
void call(X & a) {
    a.f();
}
int main() {
    X x;
    Y y;
    call(x); // يكون خرجها: "X::f()"
    call(y); // يكون خرجها: "X::f()"
}

سلوك الدوال الوهمية في المنشئات والمدمرات

قد يكون سلوك الدّوالّ الوهمية في المنشِئات (constructors) والمدمّرات (destructors) مربكًا للوهلة الأولى.

#include <iostream>

using namespace std;
class base {
    public:
        base() {
            f("base constructor");
        }~base() {
            f("base destructor");
        }
    virtual
    const char * v() {
        return "base::v()";
    }
    void f(const char * caller) {
        cout << "When called from " << caller << ", " << v() << " gets called.\n";
    }
};
class derived: public base {
    public: derived() {
        f("derived constructor");
    }~derived() {
        f("derived destructor");
    }
    const char * v() override {
        return "derived::v()";
    }
};
int main() {
    derived d;
}

الناتج:

  • عندما تُستدعى من مُنشئ أساسي (base constructor)، ستُستدعى base::v()‎.
  • عندما تُستدعى من مُنشئ مشتق (derived constructor)، سيُستدعى مشتقّ derived::v()‎.
  • عندما تُستدعى من مدمِّر مشتق، سيُستدعى derived::v()‎.
  • عندما تُستدعى من مدمِّر أساسي، سيُستدعى base::v()‎.

السبب في هذا هو أنّ الصنف المشتق ربّما يُعرِّف أعضاءً إضافيين لم تتم تهيئتهم بعد (في حالة المُنشئ) أو سبق حذفهم (في حالة المُدمّر)، ما يجعل استدعاء توابعه غير آمن. لذا يكون النوع الديناميكي لـ this* أثناء إنشاء وتدمير كائنات C++‎ هو صنف المنشئ أو المدمِّر، وليس الصنف المشتق. انظر المثال التالي:

#include <iostream>
#include <memory>

using namespace std;
class base {
    public:
        base() {
            std::cout << "foo is " << foo() << std::endl;
        }
    virtual int foo() {
        return 42;
    }
};
class derived: public base {
    unique_ptr < int > ptr_;
    public:
        derived(int i): ptr_(new int(i * i)) {}

لا يمكن استدعاء ما يلي قبل derived::derived بسبب طريقة عمل ++C:

    int foo() override {
        return *ptr_;
    }
};
int main() {
    derived d(4);
}

الدوال الوهمية الخالصة (Pure virtual functions)

تستطيع جعل دالّةً "دالّةً وهمية خالصة" (أو مُجرّدة) عبر إلحاق ‎= ‎0‎‎ بالتصريح، وتعدُّ الأصناف التي تحتوي على دالّة وهمية خالصة واحدة على الأقل أصنافًا مجرّدة، ولا يمكن إنشاء نسخ منها؛ ولا يمكن كذلك إنشاء نسخ من الأصناف المشتقة منها إلا إن كانت تعرّف، أو ترث تعريفات، كل الدوالّ الوهمية الخالصة.

struct Abstract {
    virtual void f() = 0;
};
struct Concrete {
    void f() override {}
};
Abstract a; // خطأ
Concrete c; //جيد 

حتى لو عُرِّفت دالّة على أنّها وهمية خالصة، فمن الممكن أن تُعطى تطبيقًا (implementation) افتراضيًّا، لكنّ هذا لن يغيّر حقيقة أنها مجرّدة، وسيكون على الأصناف المشتقّة أن تعرّفها قبل أن تنشئ منها نسخًا، وفي هذه الحالة يُسمح لإصدار الصنف المشتق من الدالّة باستدعاء إصدار الصنف الأساسي.

struct DefaultAbstract {
    virtual void f() = 0;
};
void DefaultAbstract::f() {}
struct WhyWouldWeDoThis: DefaultAbstract {
    void f() override {
        DefaultAbstract::f();
    }
};

هناك بعض الأسباب التي تجعلنا نرغب في فعل ذلك:

  • إذا أردنا إنشاء صنف لا يمكن استنساخه لكنّه لا يمنع الأصناف المشتقة منه من أن تُستنسَخ، نستطيع أن نعلن عن المدمّر على أنه تابع وهمي خالص، فهو على أي حال لازم التعريف إذا أردنا أن نكون قادرين على حذف النسخة من الذاكرة. ولكن لمّا كان المدمّر وهميًا، على الأرجح لمنع تسرّب الذاكرة أثناء الاستخدام متعدد الأشكال، فلن يتأثر الأداء بالسلب في حال إعلان دالّة وهمية أخرى، قد يكون هذا مفيدًا عند صنع الواجهات.
struct Interface {
    virtual~Interface() = 0;
};
Interface::~Interface() =
    default;
struct Implementation: Interface {};

في الشيفرة السابقة، لاحظ أن ()Implementation~ تُعرَّف تلقائيًا من قبل المصرِّف إن لم تُحدد بشكل صريح.

  • إذا احتوت معظم أو كلّ تطبيقات الدالّة الوهمية الخالصة على شيفرة مكررة، فيمكن نقل تلك الشيفرة إلى إصدار الدالّة الموجود في الصنف الأساسي، من أجل تسهيل الصيانة.
class SharedBase {
    State my_state;
    std::unique_ptr < Helper > my_helper;
    // ...
    public:
        virtual void config(const Context & cont) = 0;
    // ...
};
/* virtual */
void SharedBase::config(const Context & cont) {
    my_helper = new Helper(my_state, cont.relevant_field);
    do_this();
    and_that();
}
class OneImplementation: public SharedBase {
    int i;
    // ...
    public:
        void config(const Context & cont) override;
    // ...
};
void OneImplementation::config(const Context & cont) /* override */ {
    my_state = {
        cont.some_field,
        cont.another_field,
        i
    };
    SharedBase::config(cont);
    my_unique_setup();
};
// SharedBase وهكذا بالنسبة للأصناف الأخرى المشتقة من 

هذا الدرس جزء من سلسلة دروس عن C++‎.

ترجمة -بتصرّف- للفصل Chapter 38: Virtual Member Functions من كتاب C++ Notes for Professionals





تفاعل الأعضاء


لا توجد أيّة تعليقات بعد



يجب أن تكون عضوًا لدينا لتتمكّن من التعليق

انشاء حساب جديد

يستغرق التسجيل بضع ثوان فقط


سجّل حسابًا جديدًا

تسجيل الدخول

تملك حسابا مسجّلا بالفعل؟


سجّل دخولك الآن