الميراث الافتراضي

مخطط الميراث الماسي ، وهي مشكلة يحاول الميراث الافتراضي حلها.

الوراثة الافتراضية هي تقنية في لغة C++ تضمن وراثة نسخة واحدة فقط من متغيرات أعضاء الفئة الأساسية إلى الفئات المشتقة منها. بدون الوراثة الافتراضية، إذا ورثت فئتان A وB من فئة C ، وورثت فئة C من كلتا الفئتين A وB، فإن الفئة C ستحتوي على نسختين من متغيرات أعضاء الفئة C: واحدة عبر A ، والأخرى عبر B. ويمكن الوصول إلى هاتين النسختين بشكل مستقل، باستخدام نطاق المتغيرات .BCADBCDABC

Bبدلاً من ذلك، إذا ورثت الفئات Cافتراضياً من الفئة A، فإن كائنات الفئة Dستحتوي على مجموعة واحدة فقط من متغيرات الأعضاء من الفئة A.

تُعدّ هذه الخاصية مفيدة للغاية في الوراثة المتعددة ، إذ تجعل الأساس الافتراضي كائنًا فرعيًا مشتركًا للفئة المشتقة وجميع الفئات المشتقة منها. ويمكن استخدام ذلك لتجنب مشكلة المعين من خلال توضيح الغموض حول أي فئة سلف يجب استخدامها، فمن منظور الفئة المشتقة ( Dفي المثال أعلاه)، Aيعمل الأساس الافتراضي كما لو كان الفئة الأساسية المباشرة للفئة المشتقة D، وليس فئة مشتقة بشكل غير مباشر من خلال أساس ( Bأو C). [ 1 ] [ 2 ]

يُستخدم هذا الأسلوب عندما يُمثل التوريث تقييدًا لمجموعة من العناصر بدلًا من تركيب أجزائها. في لغة C++، يُشار إلى الفئة الأساسية التي يُراد لها أن تكون مشتركة في جميع أنحاء التسلسل الهرمي بأنها افتراضية باستخدام الكلمة virtualالمفتاحية `virtual` .

ضع في اعتبارك التسلسل الهرمي التالي للفئات.

UML virtual inheritance.svg

class Animal { public : virtual ~ Animal () = default ; // إظهار صراحةً أنه سيتم إنشاء مُدمِّر الفئة الافتراضي. virtual void eat () {} };class Mammal : public Animal { public : virtual void breathe () {} };class WingedAnimal : public Animal { public : virtual void flap () {} };// الخفاش هو نوع من الثدييات المجنحة class Bat : public Mammal , public WingedAnimal {};

كما ذُكر أعلاه، فإن استدعاء الدالة bat.eat()يكون غامضًا لوجود فئتين Animalأساسيتين (غير مباشرتين) في Bat، وبالتالي فإن أي Batكائن له كائنان فرعيان مختلفان من Animalالفئة الأساسية. لذلك، فإن محاولة ربط مرجع مباشرةً بالكائن Animalالفرعي لكائن Batآخر ستفشل، لأن الربط غامض بطبيعته.

Bat bat ; Animal & animal = bat ; // خطأ: أي كائن فرعي من نوع Animal يجب أن يتحول إليه Bat، // Mammal::Animal أم WingedAnimal::Animal؟

ولإزالة الغموض، يجب على المرء أن يحول بشكل صريح batإلى كائن فرعي من الفئة الأساسية:

خفاش خفاش ؛ حيوان وثديي = static_cast < Mammal &> ( خفاش ) ؛ حيوان مجنح = static_cast < WingedAnimal &> ( خفاش ) ؛

لاستدعاء الدالة eat()، يلزم نفس التوضيح أو التحديد الصريح: static_cast<Mammal&>(bat).eat()أو static_cast<WingedAnimal&>(bat).eat()أو بدلاً من ذلك bat.Mammal::eat()و bat.WingedAnimal::eat(). لا يقتصر التحديد الصريح على استخدام صيغة أسهل وأكثر توحيدًا لكل من المؤشرات والكائنات فحسب، بل يسمح أيضًا بالتوزيع الثابت، لذا يمكن القول إنه الأسلوب المفضل.

في هذه الحالة، من غير المرغوب فيه على الأرجح التوريث المزدوج Animal، لأننا نريد نمذجة أن العلاقة ( Batهو Animal) موجودة مرة واحدة فقط؛ فكون a Batهو a Mammalو a هو a WingedAnimalلا يعني أنه a Animalمرتين: Animalتتوافق الفئة الأساسية مع عقد يُنفذ Bat(علاقة " هو " أعلاه تعني في الواقع " يُنفذ متطلبات ")، و a Batيُنفذ Animalالعقد مرة واحدة فقط. المعنى العملي لعبارة " هو a مرة واحدة فقط" هو أنه Batيجب أن يكون لـ a طريقة واحدة فقط لتنفيذ Eat، وليس طريقتين مختلفتين، اعتمادًا على ما إذا كانت Mammalوجهة نظر a Batهي WingedAnimalوجهة نظر Bat. (في مثال الكود الأول، نرى أن Eatلم يتم تجاوزها في أي من Mammalأو WingedAnimal، لذلك Animalسيتصرف الكائنان الفرعيان بنفس الطريقة، لكن هذه مجرد حالة شاذة، ولا تُحدث فرقًا من وجهة نظر C++.)

يُشار إلى هذه الحالة أحيانًا باسم "الوراثة الماسية" (انظر: مشكلة الماس ) لأن مخطط الوراثة يكون على شكل ماسة. ويمكن للوراثة الافتراضية أن تساعد في حل هذه المشكلة.

الحل

يمكننا إعادة تعريف فصولنا الدراسية على النحو التالي:

class Animal { public : virtual ~ Animal () = default ; virtual void eat () {} };// فئتان ترثان الحيوان افتراضياً: class Mammal : virtual public Animal { public : virtual void breathe () {} };class WingedAnimal : virtual public Animal { public : virtual void flap () {} };// الخفاش لا يزال من الثدييات المجنحة class Bat : public Mammal , public WingedAnimal {};

أصبح جزء Animalمن Bat::WingedAnimalالآن هو نفسAnimal المثال المستخدم بواسطة Bat::Mammal، أي أن لـ مثال Batواحد مشترك فقط Animalفي تمثيله، وبالتالي فإن استدعاء الدالة Bat::Eatواضح لا لبس فيه. بالإضافة إلى ذلك، فإن التحويل المباشر من Batإلى Animalواضح لا لبس فيه أيضًا، نظرًا لوجود Animalمثال واحد فقط Batيمكن تحويله إلى .

تُمكّن إمكانية مشاركة نسخة واحدة من الكائن Animalالأصل بين Mammalالكائنين WingedAnimalمن خلال تسجيل إزاحة الذاكرة بين أعضاء الكائن Mammalالأصل WingedAnimalوأعضاء الكائن الأساسي Animalداخل الفئة المشتقة. مع ذلك، لا يمكن معرفة هذه الإزاحة في الحالة العامة إلا أثناء التشغيل، وبالتالي Batيجب أن تصبح ( vpointer, Mammal, vpointer, WingedAnimal, ). يوجد مؤشران لجدول الدوال الافتراضية ، واحد لكل تسلسل هرمي للوراثة يرث افتراضيًا من . في هذا المثال، مؤشر لـ ومؤشر لـ . بالتالي Bat، ازداد حجم الكائن بمؤشرين، ولكن الآن يوجد مؤشر واحد فقط ولا يوجد أي لبس. ستستخدم جميع الكائنات من النوع نفس مؤشرات الدوال الافتراضية، ولكن سيحتوي كل كائن على كائنه الفريد الخاص به. إذا ورثت فئة أخرى من ، مثل ، فسيكون مؤشر الدوال الافتراضية في جزء من مختلفًا عن مؤشر الدوال الافتراضية في جزء من ، على الرغم من أنهما قد يكونان متطابقين إذا كانت الفئة بنفس حجم .AnimalAnimalMammalWingedAnimalAnimalBatBatAnimalMammalSquirrelMammalSquirrelMammalBatSquirrelBat

مثال إضافي لعدة أسلاف

يوضح هذا المثال حالة Aيكون فيها للفئة الأساسية متغير منشئ msgويتم اشتقاق سلف إضافي Eمن فئة الحفيد D.

 أ / \ قبل الميلاد \ / د | هـ 

هنا، Aيجب إنشاء في كل من Dو E. علاوة على ذلك، يوضح فحص المتغير msgكيف Aتصبح الفئة فئة أساسية مباشرة لفئتها المشتقة، على عكس كونها فئة أساسية لأي فئة مشتقة وسيطة تقع بين Aوالفئة المشتقة النهائية.

استيراد std ؛باستخدام std :: string ;class A { private : string msg ; public : explicit A ( const string & s ) : msg { s } {}void test () { std :: println ( "Hello from A: {}" , msg ); } };// B، C يرثان A افتراضياً class B : virtual public A { public : B () : A ( "instance of B" ) {} };class C : virtual public A { public : C () : A ( "instance of C" ) {} };// بما أن B و C يرثان A افتراضيًا، فيجب إنشاء A في كل فئة فرعية // يمكن حذف مُنشئات B() و C() class D : public B , public C { public : D () : A ( "instance of D" ), B (), C () {} };// يمكن حذف المُنشئ D() class E : public D { public : E () : A ( "instance of E" ), D () {} };// يتعطل دون إنشاء A: // class D: public B, public C { // public: // D(): // B(), C() {} // };// يتوقف دون إنشاء A // class E: public D { // public: // E(): // D() {} // };int main ( int argc , char * argv []) { D d ; d . test (); // يطبع: "مرحباً من A: نسخة من D"E e ; e . test (); // يطبع: "مرحباً من A: نسخة من E" }

الأساليب الافتراضية البحتة

لنفترض أن دالة افتراضية بحتة مُعرَّفة في الفئة الأساسية. إذا ورثت فئة مشتقة الفئة الأساسية افتراضياً، فلا يلزم تعريف الدالة الافتراضية البحتة في تلك الفئة المشتقة. أما إذا لم ترث الفئة المشتقة الفئة الأساسية افتراضياً، فيجب تعريف جميع الدوال الافتراضية.

استيراد std ؛باستخدام std :: string ;class A { protected : string msg ; public : explicit A ( const string & s ) : msg { s } {}void test () { std :: println ( "Hello from A: {}" , msg ); } virtual void pureVirtualTest () = 0 ; };// بما أن B و C يرثان A افتراضيًا، فلا حاجة لتعريف الدالة الافتراضية البحتة pureVirtualTest. class B : virtual public A { public : explicit B ([[ maybe_unused ]] const string & s = "" ) : A ( "instance of B" ) {} };class C : virtual public A { public : explicit C ([[ maybe_unused ]] const string & s = "" ) : A ( "instance of C" ) {} };// بما أن B و C ترثان A افتراضيًا، يجب إنشاء A في كل فئة فرعية . // ومع ذلك، بما أن D لا ترث B و C افتراضيًا، يجب تعريف الدالة الافتراضية البحتة في A. class D : public B , public C { public : explicit D ([[ maybe_unused ]] const string & s = "" ) : A ( "نسخة من D من المُنشئ A" ), B ( "نسخة من D من المُنشئ B" ), C ( "نسخة من D من المُنشئ C" ) {}void pureVirtualTest () override { std :: println ( "Pure virtual hello from: {}" , msg ); } };// ليس من الضروري إعادة تعريف الدالة الافتراضية البحتة بعد تعريفها في الفئة الأصلية class E : public D { public : explicit E ([[ maybe_unused ]] const string & s = "" ) : A ( "instance of E from constructor A" ), D ( "instance of E from constructor D" ) {} };int main ( int argc , char * argv []) { D d ( "d" ); d . test (); // مرحباً من A: نسخة من D من المُنشئ A d . pureVirtualTest (); // اختبار افتراضي بحت مرحباً من: نسخة من D من المُنشئ AE e ( "e" ); e . test (); // مرحباً من A: نسخة من E من المُنشئ A e . pureVirtualTest (); // مرحباً افتراضياً بحتاً من: نسخة من E من المُنشئ A }

مراجع

  1. ميليا، أندريه. "حل مشكلة المعين باستخدام الوراثة الافتراضية" . Cprogramming.com . تاريخ الاسترجاع: 8 مارس 2010. من المشاكل التي تنشأ بسبب الوراثة المتعددة مشكلة المعين. وقد قدم بيارن ستروستروب (مبتكر لغة C++) مثالًا كلاسيكيًا على ذلك في المثال التالي:
  2. مكارديل، رالف (14 فبراير 2004). "C++/ما هو التوريث الافتراضي؟" . جميع الخبراء . مؤرشف من الأصل في 10 يناير 2010. تم الاسترجاع في 8 مارس 2010. قد يكون هذا ضروريًا عند استخدام التوريث المتعدد. في هذه الحالة، يمكن اشتقاق فئة من فئات أخرى تشترك في نفس الفئة الأساسية. في مثل هذه الحالات، بدون التوريث الافتراضي، ستحتوي كائناتك على أكثر من كائن فرعي واحد من النوع الأساسي الذي تشترك فيه الفئات الأساسية. يعتمد ما إذا كان هذا هو التأثير المطلوب على الظروف. إذا لم يكن كذلك، فيمكنك استخدام التوريث الافتراضي بتحديد فئات أساسية افتراضية لتلك الأنواع الأساسية التي يجب أن يحتوي الكائن بأكمله على كائن فرعي واحد فقط من هذه الفئة الأساسية.