سلسلة ++c للمحترفين الدرس 16: تطبيق التعددية الشكلية (Polymorphism) في Cpp


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

تعريف الأصناف متعددة الأشكال

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

الصنف الأب

لنبدأ بالصنف متعدد الأشكال (polymorphic class):

class Shape {
    public:
        virtual~Shape() =
        default;
    virtual double get_surface() const = 0;
    virtual void describe_object() const {
        std::cout << "this is a shape" << std::endl;
    }
    double get_doubled_surface() const {
        return 2 * get_surface();
    }
};

تحليل الشيفرة:

  • تستطيع تعريف السلوك متعدد الأشكال عن طريق إسباق توابع الصنف باستخدام الكلمة المفتاحية ‎virtual‎، فهنا سيُقَدَّم التابع ‎get_surface()‎ الخاص بالمربع بشكل مختلف عن ‎describe_object()‎ الخاص بالدائرة، عند استدعاء الدالة على كائن ما، ستُستدعى الدالة المقابلة للصنف الحقيقي للكائن في وقت التشغيل.
  • لا معنى لتعريف التابع ‎get_surface()‎ لشكلٍ مجرّد، لهذا تُتبَع الدالة بالتعبير ‎= 0‎، في إشارة إلى أنّ الدالة إنّما هي دالة وهمية خالصة.
  • يجب أن تعرِّف الأصناف متعددة الأشكال مُدمِّرًا وهميًا (virtual destructor) دومًا.
  • يجوز لك تعريف دوال تابعة غير وهمية، وسيُختار التابع المناسب عند استدعاء تلك التوابع على كائن ما وفقًا للصنف المستخدم في وقت التصريف، وقد عرَّفنا التابع ‎get_double_surface()‎ في المثال أعلاه بهذه الطريقة.
  • الصنف الذي يحتوي على دالة وهميّة واحدة على الأقل يُعدُّ صنفًا مجرّدًا، ولا يمكن للأصناف المجرّدة أن تُستنسخ (instantiated)، وإنما تستطيع أن تحصل فقط على مؤشرات أو مراجع إليها.

الأصناف المشتقة (Derived classes)

بمجرد تعريف صنف أساسي متعدد الأشكال فيمكنك الاشتقاق منه، انظر:

class Square: public Shape {
    Point top_left;
    double side_length;
    public:
        Square(const Point& top_left, double side): top_left(top_left), side_length(side_length) {}
    double get_surface() override {
        return side_length * side_length;
    }
    void describe_object() override {
        std::cout << "this is a square starting at " << top_left.x << ", " << top_left.y <<
            " with a length of " << side_length << std::endl;
    }
};

تفسير المثال:

  • يمكنك تعريف أو إعادة تعريف (override) أيٍّ من الدوال الوهميّة للصنف الأب، وستبقى الدوال الوهميّة في الصنف الأب وهميّة أيضًا في الصنف المشتق، ولا حاجة لإضافة الكلمة المفتاحية ‎virtual‎ مرّة أخرى، لكن يُوصى بإضافة الكلمة المفتاحية ‎override‎ إلى نهاية تصريح الدالة لمنع الأخطاء الناتجة عن الاختلافات غير الملحوظة في بصمة الدالّة.
  • إذا عُرِّفت جميع الدوالّ الوهميّة للصنف الأب، فسيكون بمقدورك استنساخ كائنات من ذلك الصنف، أما إن بقيت دوالّ وهمية غير مُعرّفة فإنّ الصنف المشتق سيبقى صنفًا مجردًا.
  • لست ملزمًا بإعادة تعريف كل الدوالّ الوهميّة، بل لا بأس بالاحتفاظ بنسخة الصنف الأب إن كان ذلك يناسب ما تحتاجه. انظر المثال التالي على الاستنساخ:
int main() {
    Square square(Point(10.0, 0.0), 6); // نعلم أنه مربع
    square.describe_object();
    std::cout << "Surface: " << square.get_surface() << std::endl;
    Circle circle(Point(0.0, 0.0), 5);
    Shape *ps = nullptr; // لا نعلم بعدُ النوع الحقيقي للكائن
    ps = &circle; // إنها دائرة، لكن كان من الممكن أن تكون مربعا
    ps->describe_object();
    std::cout << "Surface: " << ps - > get_surface() << std::endl;
}

التخفيض الآمن (Safe downcasting)

لنفترض أنّ لديك مؤشرًا يشير إلى كائن من صنف متعدد الأشكال:

Shape *ps;
ps =  get_a_new_random_shape(); // إذا لم تكن لديك هذه الدالة
// ps = new Square(0.0,0.0, 5); :يمكنك أن تكتب

فيكون التخفيض هنا هو أن تحوّل الصنف العام متعدد الأشكال ‎Shape‎ إلى أحد الأشكال المشتقة منه، مثل ‎Square‎ أو ‎Circle‎.

لماذا التخفيض؟

لن تحتاج في معظم الأحيان إلى معرفة النوع الحقيقي للكائن لأنّ الدوال الوهميّة تسمح لك بمعالجة الكائن بشكل مستقل عن نوعه:

std::cout << "Surface: " << ps->get_surface() << std::endl;

أما إذا لم تحتج إلى التخفيض فهذا مؤشّر على أنّ شيفرتك جيدة، لكن قد تحتاج إليه في بعض الأحيان حين تريد استدعاء دالة غير وهميّة لا توجد إلا في الصنف الفرعي. على سبيل المثال، يختص مفهوم القطر بالدوائر فقط، لذلك سيُعرَّف الصنف على النحو التالي:

class Circle: public Shape {
    Point center;
    double radius;
    public:
        Circle(const Point& center, double radius): center(center), radius(radius) {}
    double get_surface() const override {
        return r * r * M_PI;
    }
    // هذا صالح للدوائر وحسب، ولا معنى له بالنسبة لبقية الأشكال
    double get_diameter() const {
        return 2 * r;
    }
};

لا توجد دالة التابع ‎get_diameter()‎ إلا في الدوائر، ولم تُعرَّف لكائن (‎Shape‎) عام:

Shape* ps = get_any_shape();
ps->get_diameter(); // خطأ في التصريف

كيفية التخفيض

إذا كنت متأكدًا أنّ المؤشّر ‎ps‎ يشير إلى دائرة، فيمكنك استخدام ‎static_cast‎:

std::cout << "Diameter: " << static_cast<Circle*>(ps)->get_diameter() << std::endl;

هذا سيؤدي الغرض لكنه ينطوي على مخاطرة كبيرة، فإذا لم يكن المؤشّر ‎ps‎ يشير إلى دائرة (‎Circle‎)، فلا يمكن توقّع سلوك الشيفرة. لذلك يُفضّل استخدام الخيار الآمن ‎dynamic_cast‎ الذي يختص بالأصناف متعددة الأشكال:

int main() {
    Circle circle(Point(0.0, 0.0), 10);
    Shape &shape = circle;
    std::cout << "The shape has a surface of " << shape.get_surface() << std::endl;
    //shape.get_diameter();   // خطأ في التصريف
    Circle *pc = dynamic_cast < Circle * > ( & shape); 
// nullptr يشير إلى دائرة فسنحصل على ps إذا لم يكن المؤشر
    if (pc)
        std::cout << "The shape is a circle of diameter " << pc - > get_diameter() << std::endl;
    else
        std::cout << "The shape isn't a circle !" << std::endl;
}

لاحظ أنّ ‎dynamic_cast‎ غير ممكنة على الأصناف ذات الشكل الواحد، وستحتاج إلى دالة وهميّة واحدة على الأقل في الصنف أو أحد آبائه لتتمكن من استخدامها.

التعددية الشكلية والمدمِّرَات (Polymorphism & Destructors)

إذ أردت استخدام الصنف بحيث يكون متعدد الأشكال وتُخزَّن النسخ (instances) المشتقّة منه كمؤشّرات/مراجع أساسية، فيجب أن يكون مدمِّر الصنف الأساسي وهميًّا (‎virtual‎) أو محميًا (‎protected‎)، وفي الحالة الأولى سيؤدي تدمير الكائن إلى التحقّق من ‎vtable‎ وسَيستدعي المدمّر الصحيح تلقائيًا بناءً على النوع الديناميكي. أما في الثانية فتُعطَّل إمكانية تدمير الكائن عبر مؤشّر/مرجع للصنف الأساسي، ولا يمكن حذف الكائن إلا عند التعامل معه باعتباره من نوعه الفعلي.

struct VirtualDestructor {
    virtual~VirtualDestructor() =
        default;
};
struct VirtualDerived: VirtualDestructor {};
struct ProtectedDestructor {
    protected:
        ~ProtectedDestructor() =
        default;
};
struct ProtectedDerived: ProtectedDestructor {
    ~ProtectedDerived() =
        default;
};
// ...
VirtualDestructor* vd = new VirtualDerived;
delete vd;

ProtectedDestructor* pd = new ProtectedDerived;
delete pd; // Error: ProtectedDestructor::~ProtectedDestructor() is protected.
delete static_cast < ProtectedDerived* > (pd); // OK

سيبحث delete vd عن ()VirtualDestructor::~VirtualDestructor في vtable وعند وجوده سيبحث عن ()VirtualDerived::~VirtualDerived ويستدعيه. كذلك سيعطي delete pd خطأً لأن ()ProtectedDestructor::~ProtectedDestructor محميّ. سيضمن هذا أنّ مدمّر الصنف المشتق سيُستدعى على جميع نُسخ الصّنف المشتق، ممّا يمنع تسرّب الذاكرة.

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

ترجمة -بتصرّف- للفصل Chapter 25: Polymorphism من كتاب C++ Notes for Professionals





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


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



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

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

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


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

تسجيل الدخول

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


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