ما هو الجدول الجدول الظاهري؟

مرة واحدة في سلاك ، صادفت اختصارًا جديدًا لمسرد اختصار C ++ الخاص بي: "VTT". جودبولت :

test.o: In function `MyClass': test.cc:3: undefined reference to `VTT for MyClass' 

"VTT" في هذا السياق تعني "جدول الجدول الافتراضي". هذا هو بنية بيانات مساعدة المستخدمة (في Itanium C ++ ABI) عند إنشاء بعض الفئات الأساسية الموروثة من فئات الأساس الافتراضية. تتبع VTTs نفس قواعد التخطيط مثل الجداول الافتراضية (vtable) واكتب المعلومات (typeinfo) ، لذلك إذا حصلت على الخطأ أعلاه ، فيمكنك استبدال "vtable" عقلياً بـ "VTT" وبدء تصحيح الأخطاء. (على الأرجح ، تركت الوظيفة الرئيسية للفئة غير محددة). لنرى لماذا من الضروري استخدام VTT أو بنية مشابهة ، لنبدأ بالأساسيات.

ترتيب التصميم للميراث غير الافتراضي


عندما يكون لدينا التسلسل الهرمي للميراث ، يتم بناء الطبقات الأساسية بدءا من أبسطها . لبناء تشارلي ، يجب أن نبني أولاً فصوله الأم MrsBucket و MrBucket ، بشكل متكرر ، لبناء MrBucket ، يجب علينا أولاً بناء فصوله الأم GrandmaJosephine و GrandpaJoe.

مثل هذا:

 struct A {}; struct B : A {}; struct C {}; struct D : C {}; struct E : B, D {}; //     // ABCDE 

ترتيب التصميم لفئات الأساس الافتراضية


لكن الميراث الافتراضي يخلط بين جميع البطاقات! مع الميراث الافتراضي ، يمكن أن يكون لدينا تسلسل هرمي على شكل الماس حيث يمكن لفئتين مختلفتين من الوالدين مشاركة سلف مشترك.

 struct G {}; struct M : virtual G {}; struct F : virtual G {}; struct E : M, F {}; //     // GMFE 

في القسم الأخير ، كان كل مُنشئ مسؤولاً عن استدعاء المُنشئ لفئته الأساسية. ولكن الآن لدينا ميراث افتراضي ، ويجب على المنشئين M و F أن يعرفا بطريقة ما أنه ليس من الضروري بناء G ، لأنه شائع. إذا كانت M و F مسؤولة عن إنشاء الكائنات الأساسية في هذه الحالة ، فسيتم إنشاء الكائن الأساسي المشترك مرتين ، وهو أمر غير جيد جدًا.

للعمل مع المشروعات الفرعية للوراثة الافتراضية ، يقسم Itanium C ++ ABI كل مُنشئ إلى جزأين: مُنشئ الكائن الأساسي ومنشئ الكائن الكامل. يكون مُنشئ الكائن الأساسي مسؤولاً عن إنشاء كافة المشاريع الفرعية للوراثة غير الافتراضية (والمشروعات الفرعية الخاصة بها ، وتثبيت vptr الخاصة بهم على vtable الخاصة بهم ، وتشغيل التعليمات البرمجية في الأقواس في رمز C ++). يكون مُنشئ الكائن الكامل ، والذي يتم استدعاؤه في كل مرة تقوم فيها بإنشاء الكائن C ++ الكامل ، مسؤولًا عن إنشاء كافة العناصر الفرعية للوراثة الافتراضية للكائن المشتق ثم يقوم بالباقي.

النظر في الفرق بين مثال ABCDE لدينا من القسم السابق والمثال التالي:

 struct A {}; struct B : virtual A {}; struct C {}; struct D : virtual C {}; struct E : B, D {}; //     // ACBDE 

يستدعي مُنشئ الكائن الكامل E أولاً مُنشئات الكائن الأساسي من subobjects الظاهري A و C؛ ثم يتم استدعاء منشئي كائن الميراث الأساسي غير الظاهري B و D. لم يعد B و D مسؤولين عن إنشاء A و C ، على التوالي.

تصميم الجداول vtable


افترض أن لدينا فصلًا به بعض الأساليب الافتراضية ، على سبيل المثال ، ( Godbolt ):

 struct Cat { Cat() { poke(); } virtual void poke() { puts("meow"); } }; struct Lion : Cat { std::string roar = "roar"; Lion() { poke(); } void poke() override { roar += '!'; puts(roar.c_str()); } }; 

عندما نبني الأسد ، نبدأ ببناء الجزء الفرعي الأساسي من Cat. منشئ كات يدعو كزة (). في هذه المرحلة ، لدينا كائن Cat واحد فقط - لم نقم بعد بتهيئة بيانات العضو اللازمة لصنع كائن Lion. إذا قام مُنشئ Cat باستدعاء Lion :: poke () ، فيمكنه محاولة تغيير عضو غير مهيأ في هدير std :: string ونحصل على UB. لذا ، يُلزمنا معيار C ++ بالقيام بذلك في مُنشئ Cat ، يجب على استدعاء الأسلوب الظاهري poke () استدعاء Cat :: poke () ، وليس Lion :: poke ()!

لا توجد مشكلة يؤدي المحول البرمجي ببساطة Cat :: Cat () (كلاً من إصدار الكائن الأساسي وإصدار الكائن الكامل) إلى البدء عن طريق تعيين vptr للكائن إلى vtable للكائن Cat. سيقوم Lion :: Lion () باستدعاء Cat :: Cat () ، ثم إعادة تعيين vptr إلى المؤشر إلى vtable للكائن Cat داخل Lion ، قبل تشغيل التعليمات البرمجية بين قوسين. لا مشكلة!

إزاحة الوراثة الافتراضية


اسمحوا القط ترث تقريبا من الحيوان. بعد ذلك ، لا يقوم vtable for Cat بتخزين مؤشرات الوظائف لوظائف عضو Virtual Cat فحسب ، بل وأيضًا إزاحة المشروع الفرعي Virtual Animal داخل Cat. ( جودبولت .)

 struct Animal { const char *data = "hi"; }; struct Cat : virtual Animal { Cat() { puts(data); } }; struct Nermal : Cat {}; struct Garfield : Cat { int padding; }; 

يستعلم مُنشئ Cat بيانات العضو :: البيانات الحيوانية. إذا كان كائن Cat هذا هو الهدف الفرعي الأساسي للكائن Nermal ، فإن بيانات الأعضاء الخاصة به تكون في الإزاحة 8 ، خلف vptr مباشرة. ولكن إذا كان كائن Cat هو الهدف الفرعي الأساسي لكائن Garfield ، فإن بيانات العضو تكون في الإزاحة 16 ، خلف vptr و Garfield :: padding. للتغلب على ذلك ، يخزن Itanium ABI إزاحات الكائنات الأساسية الافتراضية في vtable للكائن Cat. يحافظ vtable for Cat-in-Nermal على حقيقة أن Animal ، وهو المشروع الفرعي Cat الأساسي ، يتم تخزينه في الإزاحة 8 ؛ يحتفظ vtable لـ Cat-in-Garfield بحقيقة أن Animal ، وهو المشروع الفرعي Cat الأساسي ، يتم تخزينه في الإزاحة 16.

الآن ادمج هذا مع القسم السابق. يجب أن يضمن المترجم أن يبدأ Cat :: Cat () (إصدار الكائن الأساسي وإصدار الكائن بالكامل) عن طريق تثبيت vptr على vtable لـ Cat-in-Nermal أو على vtable لـ Cat-in-Garfield ، اعتمادًا على النوع معظم منشأة مشتقة! ولكن كيف يعمل؟

يجب على مُنشئ الكائن الكامل للكائن الأكثر اشتقاقًا أن يحسب مسبقًا أي جدول vtable يريد أن يشير إليه vptr من المشروع الفرعي الأساسي أثناء وقت إنشاء الكائن ، ثم يجب على مُنشئ الكائن الكامل للكائن الأكثر اشتقاقًا تمرير هذه المعلومات إلى مُنشئ الكائن الأساسي للكائن الفرعي الأساسي كمعلمة خفية! دعونا نلقي نظرة على الكود الذي تم إنشاؤه لـ Cat :: Cat () ( Godbolt ):

 _ZN3CatC1Ev: #    Cat movq $_ZTV3Cat+24, (%rdi) # this->vptr = &vtable-for-Cat; retq _ZN3CatC2Ev: #     Cat movq (%rsi), %rax # fetch a value from rsi movq %rax, (%rdi) # this->vptr = *rsi; retq 

لا يقبل مُنشئ الكائن الأساسي هذه المعلمة المخفية في٪ rdi فحسب ، بل يقبل أيضًا المعلمة VTT المخفية في٪ rsi! يقوم مُنشئ الكائن الأساسي بتحميل العنوان من (٪ rsi) ويخزن العنوان في vtable للكائن Cat.

من يستدعي مُنشئ كائن Cat الأساسي يكون مسؤولاً عن توقع عنوان Cat :: Cat () الذي يجب كتابته في vptr وعن تعيين المؤشر في (٪ rsi) على هذا العنوان.

لماذا نحتاج إلى مستوى آخر من الهوية؟


النظر في مُنشئ كائن Nermal الكامل.

 _ZN3CatC2Ev: #    Cat movq (%rsi), %rax #    rsi movq %rax, (%rdi) # this->vptr = *rsi; retq _ZN6NermalC1Ev: #    Nermal pushq %rbx movq %rdi, %rbx movl $_ZTT6Nermal+8, %esi # %rsi = &VTT-for-Nermal callq _ZN3CatC2Ev #     Cat movq $_ZTV6Nermal+24, (%rbx) # this->vptr = &vtable-for-Nermal popq %rbx retq _ZTT6Nermal: .quad _ZTV6Nermal+24 # vtable-for-Nermal .quad _ZTC6Nermal0_3Cat+24 # construction-vtable-for-Cat-in-Nermal 

لماذا يقع _ZTC6Nermal0_3Cat + 24 في قسم البيانات ويتم تمرير عنوانه إلى٪ rsi ، بدلاً من تمرير _ZTC6Nermal0_3Cat + 24 مباشرةً؟

 #   ? _ZN3CatC2Ev: #     Cat movq %rsi, (%rdi) # this->vptr = rsi; retq _ZN6NermalC1Ev: #     Nermal pushq %rbx movq %rdi, %rbx movl $_ZTC6Nermal0_3Cat+24, %esi # %rsi = &construction-vtable-for-Cat-in-Nermal callq _ZN3CatC2Ev #     Cat movq $_ZTV6Nermal+24, (%rbx) # this->vptr = &vtable-for-Nermal popq %rbx retq 

هذا لأنه يمكن أن يكون لدينا عدة مستويات من الميراث! في كل مستوى من مستويات الميراث ، يجب على مُنشئ الكائن الأساسي تعيين vptr ، ثم ، ربما ، يمرر التحكم لأسفل السلسلة إلى المُنشئ الأساسي التالي ، والذي يمكنه تعيين vptrs على قيمة أخرى. هذا يتضمن قائمة أو جدول مؤشرات إلى vtable.

هنا مثال ملموس ( Godbolt ):

 struct VB { int member_of_vb = 42; }; struct Grandparent : virtual VB { Grandparent() {} }; struct Parent : Grandparent { Parent() {} }; struct Gretel : Parent { Gretel() : VB{1000} {} }; struct Hansel : Parent { int padding; Hansel() : VB{2000} {} }; 

يجب أن يقوم كائن منشئ قاعدة Grandparent بتعيين vptr الخاص به إلى Grandparent - شيء آخر ، وهو الفئة الأكثر اشتقاقًا. يجب على مُنشئ الكائن الأساسي Parent أولاً استدعاء Grandparent :: Grandparent () مع٪ rsi المناسب ، ثم تعيين vptr على Parent - شيء آخر ، وهو الفئة الأكثر اشتقاقًا. هناك طريقة لتنفيذ ذلك لـ Gretel:

 Gretel::Gretel() [  ]: pushq %rbx movq %rdi, %rbx movl $1000, 8(%rdi) # imm = 0x3E8 movl $VTT for Gretel+8, %esi callq Parent::Parent() [  ] movq $vtable for Gretel+24, (%rbx) popq %rbx retq VTT for Gretel: .quad vtable for Gretel+24 .quad construction vtable for Parent-in-Gretel+24 .quad construction vtable for Grandparent-in-Gretel+24 

يمكنك أن ترى في Godbolt أن مُنشئ الكائن الأساسي للفئة Parent يستدعي أولاً Grandparent :: Grandparent () مع٪ rsi + 8 ، ثم يعين vptr الخاص به على (٪ rsi). لذلك ، نحن هنا نستخدم حقيقة أن جريتيل ، إذا جاز التعبير ، وضعت بعناية طريقًا من فتات الخبز التي اتبعتها جميع الطبقات الأساسية أثناء البناء.

يتم استخدام نفس VTT في المدمرة ( Godbolt ). حسب علمي ، لا يتم استخدام الصف الفارغ لجدول VTT. يقوم مُنشئ Gretel بتحميل الـ vtable لـ Gretel + 24 إلى vptr ، لكنه يعلم أن هذا العنوان ثابت ، ولا يحتاج إلى تحميله من VTT. أعتقد أنه تم الحفاظ على صف صف الجدول ببساطة لأسباب تاريخية. (وبالطبع ، لا يمكن للمترجم التخلص منه ، لأنه سيكون انتهاكًا لـ Itanium ABI وسيكون من المستحيل الارتباط بالكود القديم الذي يتقيد بـ Itanium-ABI).

هذا كل شيء ، نظرنا إلى جدول الجداول الافتراضية ، أو VTT.

مزيد من المعلومات


يمكنك العثور على معلومات VTT في هذه الأماكن:

StackOverflow: " ما هو VTT لفئة؟ "
" ملاحظات VTable على الوراثة المتعددة في مترجم GCC C ++ v4.0.1 " (مورغان ديترز ، 2005)
The Itanium C ++ ABI ، قسم "طلب VTT"

أخيرًا ، يجب أن أؤكد مجددًا أن VTT هي إحدى ميزات Itanium C ++ ABI ، وتستخدم على Linux ، OSX ، إلخ. لا يحتوي MSVC ABI المستخدم على Windows على VTT ، ويستخدم آلية مختلفة تمامًا عن الميراث الافتراضي. لا أعرف (حتى الآن) أي شيء تقريبًا عن MSVC ABI ، لكن ربما في يوم ما سأكتشف كل شيء وأكتب منشورًا عنه!

Source: https://habr.com/ru/post/ar474318/


All Articles