جدول الأساليب الافتراضية
في برمجة الحاسوب ، فإن جدول الأساليب الافتراضية ( VMT )، أو جدول الوظائف الافتراضية ، أو جدول الاستدعاءات الافتراضية ، أو جدول الإرسال ، أو vtable ، أو vftable هي آلية تستخدم في لغة البرمجة لدعم الإرسال الديناميكي (أو ربط الأساليب في وقت التشغيل ).
عندما تُعرّف فئةٌ ما دالةً (أو طريقةً ) افتراضية، تُضيف معظم المُترجمات مُتغيرًا عضويًا مخفيًا إلى الفئة يُشير إلى مصفوفة من المؤشرات إلى الدوال (الافتراضية) تُسمى جدول الطرق الافتراضية. تُستخدم هذه المؤشرات أثناء التشغيل لاستدعاء تطبيقات الدوال المُناسبة، لأنه في وقت الترجمة قد لا يكون معروفًا بعد ما إذا كان سيتم استدعاء الدالة الأساسية أو دالة مُشتقة تُنفذها فئةٌ ترث من الفئة الأساسية.
توجد طرق عديدة لتنفيذ هذا التوزيع الديناميكي، لكن استخدام جداول الأساليب الافتراضية شائعٌ بشكل خاص في لغة C++ واللغات المشابهة (مثل D و C# ). كما تميل اللغات التي تفصل واجهة برمجة الكائنات عن تنفيذها، مثل Visual Basic و Delphi ، إلى استخدام هذا الأسلوب، لأنه يسمح للكائنات باستخدام تنفيذ مختلف بمجرد استخدام مجموعة مختلفة من مؤشرات الأساليب. يتيح هذا الأسلوب إنشاء مكتبات خارجية، وهو ما قد لا تسمح به تقنيات أخرى. [ 1 ]
لنفترض أن برنامجًا يحتوي على ثلاث فئات في تسلسل هرمي للوراثة: فئة أساسية ، Catوفئتين فرعيتين ، HouseCatو Lion. تُعرّف الفئة Cat دالة افتراضية باسم speak speak()، لذا يمكن لفئاتها الفرعية توفير تنفيذ مناسب لها (مثلًا، إما meow()أو roar()). عندما يستدعي البرنامج الدالة speak على مرجع Cat (الذي يمكن أن يشير إلى نسخة من ، أو نسخة من أو )، يجب أن يكون الكود قادرًا على تحديد أي تنفيذ للدالة يجب توجيه الاستدعاء إليه. يعتمد هذا على الفئة الفعلية للكائن، وليس على فئة المرجع إليه ( ). لا يمكن عمومًا تحديد الفئة بشكل ثابت (أي في وقت الترجمة )، وبالتالي لا يمكن للمترجم أيضًا تحديد الدالة التي يجب استدعاؤها في ذلك الوقت. يجب توجيه الاستدعاء إلى الدالة الصحيحة ديناميكيًا (أي في وقت التشغيل ).CatHouseCatLionCat
تطبيق
يحتوي جدول الأساليب الافتراضية للكائن على عناوين الأساليب المرتبطة ديناميكيًا. تُستدعى الأساليب بجلب عنوان الأسلوب من جدول الأساليب الافتراضية للكائن. يكون جدول الأساليب الافتراضية واحدًا لجميع الكائنات التي تنتمي إلى نفس الفئة، ولذلك يُشارك عادةً بينها. تمتلك الكائنات التي تنتمي إلى فئات متوافقة من حيث النوع (على سبيل المثال، الأشقاء في تسلسل هرمي للوراثة) جداول أساليب افتراضية بنفس التخطيط: يظهر عنوان أسلوب معين عند نفس الإزاحة لجميع الفئات المتوافقة من حيث النوع. وبالتالي، فإن جلب عنوان الأسلوب من إزاحة معينة إلى جدول الأساليب الافتراضية سيؤدي إلى الحصول على الأسلوب المقابل للفئة الفعلية للكائن. [ 2 ]
لا تفرض معايير C++ بالضبط كيفية تنفيذ الإرسال الديناميكي، ولكن المترجمات تستخدم عمومًا اختلافات طفيفة على نفس النموذج الأساسي.
عادةً، يُنشئ المُصرّف جدولًا منفصلاً للأساليب الافتراضية لكل فئة. عند إنشاء كائن، يُضاف مؤشر إلى هذا الجدول، يُسمى مؤشر الجدول الافتراضي ( vpointer أو VPTR) ، كعضو مخفي في هذا الكائن. ولذلك، يجب على المُصرّف أيضًا توليد شيفرة "مخفية" في مُنشئات كل فئة لتهيئة مؤشر الجدول الافتراضي للكائن الجديد إلى عنوان جدول الأساليب الافتراضية لتلك الفئة.
تضع العديد من المترجمات مؤشر الجدول الافتراضي كآخر عنصر في الكائن؛ بينما تضعه مترجمات أخرى كأول عنصر؛ ويعمل الكود المصدري القابل للنقل في كلتا الحالتين. [ 3 ] على سبيل المثال، كان g++ يضع المؤشر سابقًا في نهاية الكائن. [ 4 ]
مثال
ضع في اعتبارك تعريفات الفئات التالية في لغة C++ :
استيراد std ؛class Base1 { private : int b1 = 0 ; public : explicit Base1 ( int b1 ) : b1 { b1 } {}افتراضي ~ Base1 () = افتراضي ؛void nonVirtual () { std :: println ( "تم استدعاء Base1::nonVirtual()!" ); }virtual void fn1 () { std :: println ( "تم استدعاء Base1::fn1()!" ); } };class Base2 { private : int b2 = 0 ; public : explicit Base2 ( int b2 ) : b2 { b2 } {}افتراضي ~ Base2 () = افتراضي ؛virtual void fn2 () { std :: println ( "تم استدعاء Base2::fn2()!" ); } };class Derived : public Base1 , public Base2 { private : int d = 0 ; public : explicit Derived ( int b1 , int b2 , int d ) : Base1 ( b1 ), Base2 ( b2 ), d { d } {}~ مشتق () = افتراضي ؛void fn3 () { std :: println ( "تم استدعاء Derived::fn3()!" ); }void fn2 () override { std :: println ( "تم استدعاء Derived::fn2()!" ); } };int main () { Base2 * base2 = new Base2 (); Derived * derived = new Derived ();// ...حذف الأساس 2 ؛ حذف المشتق ؛ }ينتج g++ 3.4.6 من GCC تخطيط الذاكرة التالي ذو 32 بت للكائن base2: [ nb 1 ]
ب2: +0: مؤشر إلى جدول الأساليب الافتراضية لـ Base2 +4: قيمة b2 جدول الطرق الافتراضية للنظام الأساسي 2: +0: Base2::fn2()
وتخطيط الذاكرة التالي للكائن derived:
مشتق: +0: مؤشر إلى جدول الأساليب الافتراضية لـ Derived (لـ Base1) +4: قيمة b1 +8: مؤشر إلى جدول الأساليب الافتراضية للدالة المشتقة (للأساس 2) +12: قيمة b2 +16: قيمة د الحجم الإجمالي: 20 بايت. جدول الطرق الافتراضية للدالة المشتقة (للأساس 1): +0: Base1::fn1() // لم يتم تجاوز Base1::fn1() جدول الطرق الافتراضية للدالة المشتقة (للأساس 2): +0: Derived::fn2() // يتم تجاوز Base2::fn2() بواسطة Derived::fn2()
لاحظ أن الدوال التي لا تحمل الكلمة المفتاحية virtualفي تعريفها (مثل nonVirtual()و d()) لا تظهر عادةً في جدول الدوال الافتراضية. توجد استثناءات لحالات خاصة كما هو الحال مع المُنشئ الافتراضي .
لاحظ أيضًا المُدمِّرات الافتراضية في الفئات الأساسية، Base1و Base2. فهي ضرورية لضمان delete derived;تحرير الذاكرة ليس فقط لـ Derived، بل أيضًا لـ Base1و Base2، إذا derivedكان مؤشرًا أو مرجعًا للأنواع Base1أو B2. وقد تم استبعادها من تخطيطات الذاكرة لتبسيط المثال. [ ملاحظة 2 ]
يتم تنفيذ تجاوز الطريقةfn2() في الفئة Derivedعن طريق تكرار جدول الطرق الافتراضية Base2واستبدال المؤشر بمؤشر Base2::fn2()إلى Derived::fn2().
الوراثة المتعددة والوظائف غير المتزامنة
يُطبّق مُصرّف g++ التوريث المتعدد للفئات Base1، بما Base2في ذلك الفئة الأساسية، Derivedباستخدام جدولين للأساليب الافتراضية، أحدهما لكل فئة أساسية. (توجد طرق أخرى لتطبيق التوريث المتعدد، لكن هذه الطريقة هي الأكثر شيوعًا). ويؤدي هذا إلى ضرورة إجراء "تصحيحات للمؤشرات"، والتي تُسمى أيضًا " الدوال المُؤقتة " ، عند تحويل الأنواع .
انظر إلى كود C++ التالي:
Derived * derived = new Derived (); Base1 * base1 = derived ; Base2 * base2 = derived ;بينما سيشير كل derivedمن و إلى نفس موقع الذاكرة بعد تنفيذ هذا الكود، سيشير إلى موقع آخر (ثمانية بايتات بعد موقع الذاكرة الخاص بـ ). وبالتالي، يشير إلى المنطقة داخل التي "تبدو" كنسخة من ، أي أن لها نفس تخطيط الذاكرة الخاص بنسخة من .base1base2derived + 8derivedbase2derivedBase2Base2
دعاء
derived->fn1()يتم التعامل مع الاستدعاء عن طريق إلغاء مرجعية المؤشر derivedالظاهري Derived::Base1، والبحث عن fn1الإدخال في جدول الأساليب الافتراضية، ثم إلغاء مرجعية هذا المؤشر لاستدعاء الكود.
الميراث الفردي
في حالة الوراثة الفردية (أو في لغة ذات وراثة فردية فقط)، إذا كان المؤشر vpointer هو العنصر الأول دائمًا derived(كما هو الحال مع العديد من المترجمات)، فإن هذا يختزل إلى لغة C++ الزائفة التالية:
( * (( * مشتق )[ 0 ]))( مشتق )يشير *derivedإلى جدول الأساليب الافتراضية لـ، ويشيرDerived إلى الأسلوب الأول في جدول الأساليب الافتراضية. يصبح [0]المعامل مؤشرًا إلى الكائن.derivedthis
الوراثة المتعددة
في الحالة الأكثر عمومية، يكون استدعاء " Base1::fn1()أو" Derived::fn2()أكثر تعقيداً:
// استدعاء الدالة fn1() المشتقة ( * ( * ( المشتقة [ 0 ] /*مؤشر إلى جدول الدوال الافتراضية للدالة المشتقة (للفئة الأساسية 1)*/ )[ 0 ]))( المشتقة )// استدعاء derived->fn2() ( * ( * ( derived [ 8 ] /*مؤشر إلى جدول الأساليب الافتراضية لـ Derived (للأساس 2)*/ )[ 0 ]))( derived + 8 )derived->fn1()يُمرر الاستدعاء Base1مؤشرًا كمعامل. derived->fn2()يُمرر الاستدعاء Base2مؤشرًا كمعامل. يتطلب هذا الاستدعاء الثاني تعديلًا لإنتاج المؤشر الصحيح. موقع المؤشر Base2::fn2غير موجود في جدول الأساليب الافتراضية للدالة Derived.
بالمقارنة، derived->fnonvirtual()فإن إجراء مكالمة إلى أمر أبسط بكثير:
( * Base1 :: fnonvirtual )( مشتق )كفاءة
يتطلب استدعاء الدوال الافتراضية على الأقل عملية فك مرجعية إضافية مُفهرسة، وأحيانًا إضافة "تصحيحية"، مقارنةً باستدعاء الدوال غير الافتراضية، الذي يقتصر على الانتقال إلى مؤشر مُضمّن في البرنامج. لذا، فإن استدعاء الدوال الافتراضية أبطأ بطبيعته من استدعاء الدوال غير الافتراضية. تشير تجربة أُجريت عام ١٩٩٦ إلى أن ما يقارب ٦-١٣٪ من وقت التنفيذ يُستغرق في توجيه البرنامج إلى الدالة الصحيحة، مع العلم أن العبء الإضافي قد يصل إلى ٥٠٪. [ ٥ ] قد لا تكون تكلفة الدوال الافتراضية مرتفعةً جدًا في معالجات الحاسوب الحديثة نظرًا لسعة ذاكرة التخزين المؤقت الأكبر بكثير ودقة التنبؤ بالتفرعات الأفضل .
علاوة على ذلك، في البيئات التي لا يُستخدم فيها تجميع JIT ، لا يمكن عادةً تضمين استدعاءات الدوال الافتراضية . في بعض الحالات، قد يكون من الممكن للمُجمِّع تنفيذ عملية تُعرف باسم إلغاء الافتراضية ، حيث يتم، على سبيل المثال، استبدال البحث والاستدعاء غير المباشر بتنفيذ مشروط لكل جسم مُضمَّن، ولكن هذه التحسينات ليست شائعة.
ولتجنب هذا العبء الإضافي، تتجنب المترجمات عادةً استخدام جداول الأساليب الافتراضية كلما أمكن حل الاستدعاء في وقت الترجمة .
لذا، قد لا يتطلب استدعاء الدالة المذكورة fn1أعلاه البحث في جدول، لأن المترجم قد يكون قادرًا على تحديد أن المتغير derivedلا يمكنه استيعاب سوى قيمة واحدة Derivedفي هذه المرحلة، Derivedولا يقوم بتجاوز الدالة الأخرى fn1. أو قد يتمكن المترجم (أو المُحسِّن) من اكتشاف عدم وجود فئات فرعية من الدالة في Base1أي مكان في البرنامج تقوم بتجاوز الدالة الأخرى fn1. من المحتمل ألا يتطلب استدعاء الدالة Base1::fn1أو Base2::fn2الدالة الأخرى البحث في جدول، لأن التنفيذ مُحدد بشكل صريح (مع أنه لا يزال يتطلب thisإصلاح مؤشر ).
مقارنة بالبدائل
يُعد جدول الأساليب الافتراضية عمومًا حلاً وسطًا جيدًا للأداء لتحقيق التوزيع الديناميكي، ولكن توجد بدائل، مثل توزيع الشجرة الثنائية ، ذات أداء أعلى في بعض الحالات النموذجية، ولكن بمقايضات مختلفة. [ 1 ] [ 6 ]
ومع ذلك، فإن جداول الأساليب الافتراضية تسمح فقط بالإرسال الفردي على المعلمة الخاصة "this"، على عكس الإرسال المتعدد (كما هو الحال في CLOS أو Dylan أو Julia )، حيث يمكن أخذ أنواع جميع المعلمات في الاعتبار عند الإرسال.
تعمل جداول الأساليب الافتراضية أيضًا فقط إذا كان الإرسال مقيدًا بمجموعة معروفة من الأساليب، لذلك يمكن وضعها في مصفوفة بسيطة يتم إنشاؤها في وقت الترجمة، على عكس لغات الكتابة الديناميكية (مثل Smalltalk أو Python أو JavaScript ).
تعتمد اللغات التي توفر إحدى هاتين الميزتين أو كلتيهما على البحث عن سلسلة نصية في جدول تجزئة ، أو أي طريقة مكافئة أخرى. وهناك تقنيات متنوعة لتسريع هذه العملية (مثل تخزين أسماء الدوال في الذاكرة المؤقتة/تقسيمها إلى رموز، وتخزين عمليات البحث مؤقتًا، والترجمة الفورية ).
انظر أيضاً
ملحوظات
- يمكن استخدام وسيطة G++
-fdump-class-hierarchy(بدءًا من الإصدار 8-fdump-lang-class) لعرض جداول الدوال الافتراضية للفحص اليدوي. بالنسبة لمترجم VisualAge XlC لنظام AIX، استخدمها-qdump_class_hierarchyلعرض تسلسل الفئات وتخطيط جدول الدوال الافتراضية. - ↑ "C++ - لماذا يوجد اثنان من المدمرات الافتراضية في الجدول الافتراضي وأين عنوان الدالة غير الافتراضية (gcc4.6.3)" .
مراجع
- مارغريت أ. إليس وبيارن ستروستروب (1990). دليل مرجعي مُشروح للغة سي++. ريدينغ، ماساتشوستس: أديسون-ويسلي. ( ISBN) 0-201-51459-1)
- 1 2 زيندرا، أوليفييه؛ كولنيت، دومينيك؛ كولين، سوزان (1997). إرسال ديناميكي فعال بدون جداول الوظائف الافتراضية: مترجم SmallEiffel -- مؤتمر ACM SIGPLAN السنوي الثاني عشر حول أنظمة البرمجة الشيئية واللغات والتطبيقات (OOPSLA'97)، ACM SIGPLAN، أكتوبر 1997، أتلانتا، الولايات المتحدة. ص 125-141. انريا-00565627 . مركز البحوث المعلوماتية في نانسي الحرم الجامعي العلمي، باتيمنت لوريا. ص. 16.
- ↑ إليس وستروستروب 1990، الصفحات 227-232
- ↑ داني كاليف. "دليل مرجعي للغة C++: نموذج الكائن II" . 2003. العنوان "الوراثة وتعدد الأشكال" و"الوراثة المتعددة".
- ↑ "مشكلات مغلقة في واجهة التطبيق الثنائية للغة C++" . مؤرشف من الأصل بتاريخ 25 يوليو 2011. تم الاطلاع عليه بتاريخ 17 يونيو 2011 .
{{cite web}}: CS1 maint: bot: حالة عنوان URL الأصلي غير معروفة ( رابط ) - ^ دريسن، كاريل. هولزل، أورس (1996). "التكلفة المباشرة لاستدعاءات الوظائف الافتراضية في C++" (PDF) . OOPSLA.
- ↑ زيندرا، أوليفييه ودريسن، كاريل، "اختبار إجهاد هياكل التحكم للإرسال الديناميكي في جافا" ، الصفحات 105-118، وقائع ندوة USENIX الثانية لأبحاث وتكنولوجيا آلة جافا الافتراضية، 2002 (JVM '02)
- طريقة (برمجة الحاسوب)
