C ++ vtables. الجزء 2 (الوراثة الافتراضية + الشفرة التي تولدها المترجم)

تم إعداد ترجمة المقال خاصة لطلاب الدورة التدريبية "C ++ Developer" . هل هو مثير للاهتمام لتطوير في هذا الاتجاه؟ شاهد تسجيل فئة اختبار Google Framework Framework Practice !



الجزء 3 - الوراثة الافتراضية


في الجزءين الأول والثاني من هذه المقالة ، تحدثنا عن كيفية عمل vtables في أبسط الحالات ، ثم في الميراث المتعدد. الميراث الافتراضي يعقد الوضع أكثر.


كما تذكر ، الوراثة الافتراضية تعني أنه في فئة معينة لا يوجد سوى مثيل واحد من الفئة الأساسية. على سبيل المثال:


class ios ... class istream : virtual public ios ... class ostream : virtual public ios ... class iostream : public istream, public ostream 

إذا لم تكن الكلمة الأساسية virtual أعلاه ، iostream فعليًا على حالتين من ios يمكن أن يتسببان في حدوث صداع أثناء المزامنة وسيكون ببساطة غير فعال.


لفهم الوراثة الافتراضية ، سننظر في جزء التعليمات البرمجية التالي:


 #include <iostream> using namespace std; class Grandparent { public: virtual void grandparent_foo() {} int grandparent_data; }; class Parent1 : virtual public Grandparent { public: virtual void parent1_foo() {} int parent1_data; }; class Parent2 : virtual public Grandparent { public: virtual void parent2_foo() {} int parent2_data; }; class Child : public Parent1, public Parent2 { public: virtual void child_foo() {} int child_data; }; int main() { Child child; } 

دعونا استكشاف child . سأبدأ بإلقاء قدر كبير من الذاكرة على وجه الدقة حيث يبدأ vtable Child ، كما فعلنا في الأجزاء السابقة ، ومن ثم تحليل النتائج. أقترح إلقاء نظرة سريعة على النتيجة هنا والعودة إليها عندما أفصح عن التفاصيل أدناه.


 (gdb) p child $1 = {<Parent1> = {<Grandparent> = {_vptr$Grandparent = 0x400998 <vtable for Child+96>, grandparent_data = 0}, _vptr$Parent1 = 0x400950 <vtable for Child+24>, parent1_data = 0}, <Parent2> = {_vptr$Parent2 = 0x400978 <vtable for Child+64>, parent2_data = 4195888}, child_data = 0} (gdb) x/600xb 0x400938 0x400938 <vtable for Child>: 0x20 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400940 <vtable for Child+8>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400948 <vtable for Child+16>: 0x00 0x0b 0x40 0x00 0x00 0x00 0x00 0x00 0x400950 <vtable for Child+24>: 0x70 0x08 0x40 0x00 0x00 0x00 0x00 0x00 0x400958 <vtable for Child+32>: 0xa0 0x08 0x40 0x00 0x00 0x00 0x00 0x00 0x400960 <vtable for Child+40>: 0x10 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400968 <vtable for Child+48>: 0xf0 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0x400970 <vtable for Child+56>: 0x00 0x0b 0x40 0x00 0x00 0x00 0x00 0x00 0x400978 <vtable for Child+64>: 0x90 0x08 0x40 0x00 0x00 0x00 0x00 0x00 0x400980 <vtable for Child+72>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400988 <vtable for Child+80>: 0xe0 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0x400990 <vtable for Child+88>: 0x00 0x0b 0x40 0x00 0x00 0x00 0x00 0x00 0x400998 <vtable for Child+96>: 0x80 0x08 0x40 0x00 0x00 0x00 0x00 0x00 0x4009a0 <VTT for Child>: 0x50 0x09 0x40 0x00 0x00 0x00 0x00 0x00 0x4009a8 <VTT for Child+8>: 0xf8 0x09 0x40 0x00 0x00 0x00 0x00 0x00 0x4009b0 <VTT for Child+16>: 0x18 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x4009b8 <VTT for Child+24>: 0x98 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x4009c0 <VTT for Child+32>: 0xb8 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x4009c8 <VTT for Child+40>: 0x98 0x09 0x40 0x00 0x00 0x00 0x00 0x00 0x4009d0 <VTT for Child+48>: 0x78 0x09 0x40 0x00 0x00 0x00 0x00 0x00 0x4009d8: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x4009e0 <construction vtable for Parent1-in-Child>: 0x20 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x4009e8 <construction vtable for Parent1-in-Child+8>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x4009f0 <construction vtable for Parent1-in-Child+16>: 0x50 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x4009f8 <construction vtable for Parent1-in-Child+24>: 0x70 0x08 0x40 0x00 0x00 0x00 0x00 0x00 0x400a00 <construction vtable for Parent1-in-Child+32>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400a08 <construction vtable for Parent1-in-Child+40>: 0xe0 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0x400a10 <construction vtable for Parent1-in-Child+48>: 0x50 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400a18 <construction vtable for Parent1-in-Child+56>: 0x80 0x08 0x40 0x00 0x00 0x00 0x00 0x00 0x400a20 <typeinfo name for Parent1>: 0x37 0x50 0x61 0x72 0x65 0x6e 0x74 0x31 0x400a28 <typeinfo name for Parent1+8>: 0x00 0x31 0x31 0x47 0x72 0x61 0x6e 0x64 0x400a30 <typeinfo name for Grandparent+7>: 0x70 0x61 0x72 0x65 0x6e 0x74 0x00 0x00 0x400a38 <typeinfo for Grandparent>: 0x50 0x10 0x60 0x00 0x00 0x00 0x00 0x00 0x400a40 <typeinfo for Grandparent+8>: 0x29 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400a48: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400a50 <typeinfo for Parent1>: 0xa0 0x10 0x60 0x00 0x00 0x00 0x00 0x00 0x400a58 <typeinfo for Parent1+8>: 0x20 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400a60 <typeinfo for Parent1+16>: 0x00 0x00 0x00 0x00 0x01 0x00 0x00 0x00 0x400a68 <typeinfo for Parent1+24>: 0x38 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400a70 <typeinfo for Parent1+32>: 0x03 0xe8 0xff 0xff 0xff 0xff 0xff 0xff 0x400a78: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400a80 <construction vtable for Parent2-in-Child>: 0x10 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400a88 <construction vtable for Parent2-in-Child+8>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400a90 <construction vtable for Parent2-in-Child+16>: 0xd0 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400a98 <construction vtable for Parent2-in-Child+24>: 0x90 0x08 0x40 0x00 0x00 0x00 0x00 0x00 0x400aa0 <construction vtable for Parent2-in-Child+32>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400aa8 <construction vtable for Parent2-in-Child+40>: 0xf0 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0x400ab0 <construction vtable for Parent2-in-Child+48>: 0xd0 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400ab8 <construction vtable for Parent2-in-Child+56>: 0x80 0x08 0x40 0x00 0x00 0x00 0x00 0x00 0x400ac0 <typeinfo name for Parent2>: 0x37 0x50 0x61 0x72 0x65 0x6e 0x74 0x32 0x400ac8 <typeinfo name for Parent2+8>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400ad0 <typeinfo for Parent2>: 0xa0 0x10 0x60 0x00 0x00 0x00 0x00 0x00 0x400ad8 <typeinfo for Parent2+8>: 0xc0 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400ae0 <typeinfo for Parent2+16>: 0x00 0x00 0x00 0x00 0x01 0x00 0x00 0x00 0x400ae8 <typeinfo for Parent2+24>: 0x38 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400af0 <typeinfo for Parent2+32>: 0x03 0xe8 0xff 0xff 0xff 0xff 0xff 0xff 0x400af8 <typeinfo name for Child>: 0x35 0x43 0x68 0x69 0x6c 0x64 0x00 0x00 0x400b00 <typeinfo for Child>: 0xa0 0x10 0x60 0x00 0x00 0x00 0x00 0x00 0x400b08 <typeinfo for Child+8>: 0xf8 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400b10 <typeinfo for Child+16>: 0x02 0x00 0x00 0x00 0x02 0x00 0x00 0x00 0x400b18 <typeinfo for Child+24>: 0x50 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400b20 <typeinfo for Child+32>: 0x02 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400b28 <typeinfo for Child+40>: 0xd0 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400b30 <typeinfo for Child+48>: 0x02 0x10 0x00 0x00 0x00 0x00 0x00 0x00 0x400b38 <vtable for Grandparent>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400b40 <vtable for Grandparent+8>: 0x38 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400b48 <vtable for Grandparent+16>: 0x80 0x08 0x40 0x00 0x00 0x00 0x00 0x00 

واو ، هناك الكثير من المعلومات. يطفو سؤالان جديدان على الفور: ما هو VTT وما هو البناء vtable for X-in-Child ؟ سوف نجيب عليها قريباً بما فيه الكفاية.
لنبدأ ببنية الذاكرة التابعة:


حجمقيمة
8 بايت_vptr $ Parent1
4 بايتparent1_data (+ 4 بايتات للحشو)
8 بايت_vptr $ Parent2
4 بايتparent2_data
4 بايتchild_data
8 بايت_vptr $ الجد
4 بايتgrandparent_data (+ 4 بايتات ملء)

في الواقع ، لدى Child مثيل واحد فقط من الأجداد. الشيء غير المميز هو أنه الأخير في الذاكرة ، على الرغم من أنه الأعلى في التسلسل الهرمي.
هنا هو هيكل vtable :


العنوانقيمةمحتوى
0x4009380x20 (32)إزاحة القاعدة الافتراضية (سنناقش هذا قريبًا)
0x4009400top_offset
0x4009480x400b00typeinfo للطفل
0x4009500x400870Parent1 :: parent1_foo (). مؤشر vtable Parent1 يشير هنا.
0x4009580x4008a0الطفل :: child_foo ()
0x4009600x10 (16)إزاحة قاعدة افتراضية
0x400968-16top_offset
0x40090x400btypeinfo للطفل
7000
0x4009780x400890Parent2 :: parent2_foo (). يشير مؤشر vtable Parent2 هنا.
0x4009800إزاحة قاعدة افتراضية
0x400988-32top_offset
0x4009900x400b00typeinfo للطفل
0x4009980x400880Grandparent :: grandparent_foo (). يشير مؤشر vtable Grandparent هنا.

أعلاه هناك مفهوم جديد - virtual-base offset . قريبا سوف نفهم ما يفعله هناك.
بعد ذلك ، دعونا استكشاف هذه construction vtables غريبة المظهر. هنا هو بناء vtable for Parent1-in-Child :


قيمةمحتوى
0x20 (32)إزاحة قاعدة افتراضية
0أعلى تعويض
0x400a50typeinfo ل Parent1
0x400870Parent1 :: parent1_foo ()
0إزاحة قاعدة افتراضية
-32أعلى تعويض
0x400a50typeinfo ل Parent1
0x400880Grandparent :: grandparent_foo ()

في الوقت الحالي ، أعتقد أنه سيكون من المفهوم لوصف العملية أكثر من تكديس المزيد من الجداول بأرقام عشوائية عليك. لذلك:


تخيل أنك Child . يطلب منك بناء نفسك في قطعة جديدة من الذاكرة. نظرًا لأنك ترث Grandparent مباشرة (وهو ما يعنيه الميراث الافتراضي) ، فسوف تتصل مباشرة Parent1 مباشرة (إذا لم يكن الوراثة الافتراضية ، Parent1 ، والذي بدوره سوف يطلق على مُنشئ Grandparent ). قمت بتعيين this += 32 بايت ، لأن هذا هو المكان الذي توجد فيه بيانات Grandparent ، وتدعو المنشئ. بسيط جدا


ثم حان الوقت لبناء Parent1 . يستطيع Parent1 أن يفترض بأمان أنه بحلول الوقت الذي يبني فيه نفسه ، فإن Grandparent قد تم إنشاؤه بالفعل ، حتى يتمكن ، على سبيل المثال ، من الوصول إلى بيانات وطرق Grandparent . لكن مهلا ، كيف يعرف أين يجد هذه البيانات؟ فهي ليست في نفس المكان مع المتغيرات Parent1 !


يدخل construction table for Parent1-in-Child إلى مكان الحادث. هذا الجدول مخصص لإخبار Parent1 أين يمكن العثور على أجزاء من البيانات يمكنه الوصول إليها. this يشير إلى بيانات Parent1 . يشير virtual-base offset إلى حيث يمكنك العثور على بيانات Grandparent: الخطوة 32 بايت للأمام من هذا وستجد ذاكرة Grandparent . هل حصلت عليه؟ يشبه الإزاحة الافتراضية الأساسية top_offset ، لكن بالنسبة للفصول الافتراضية.


الآن وقد فهمنا ذلك ، فإن بناء Parent2 هو نفسه بشكل أساسي ، فقط باستخدام construction table for Parent2-in-Child . في الواقع ، Parent2-in-Child على virtual-base offset من 16 بايت.


دع المعلومات تنقع قليلاً. هل أنت مستعد للمتابعة؟ حسنا.
الآن دعنا نعود إلى VTT . هنا هو هيكل VTT :


العنوانقيمةرمزمحتوى
0x4009a00x400950vtable للطفل + 24إدخالات Parent1 في vtable التابعة
0x4009a80x4009f8vtable البناء لـ Parent1-in-Child + 24طرق Parent1 في Parent1-in-Child
0x4009b00x400a18vtable البناء لـ Parent1-in-Child + 56طرق الأجداد ل Parent1 في الطفل
0x4009b80x400a98vtable البناء لـ Parent2-in-Child + 24طرق Parent2 في Parent2-in-Child
0x4009c00x400ab8vtable البناء لـ Parent2-in-Child + 56`أساليب الأجداد ل Parent2 في الطفل
0x4009c80x400998vtable للطفل + 96`إدخالات الجد في الطفل vtable
0x4009d00x400978vtable للطفل + 64`إدخالات Parent2 في الطفل vtable

VTT تعني virtual-table table ، مما يعني أنه vtable. هذا هو جدول ترجمة يعرف ، على سبيل المثال ، ما إذا كان المُنشئ Parent1 لكائن فردي ، أو Parent1-in-SomeOtherObject ، أو Parent1-in-SomeOtherObject . يظهر دائمًا بعد vtable مباشرةً ، بحيث يعرف المترجم مكان العثور عليه. لذلك ، ليست هناك حاجة لتخزين مؤشر آخر في الكائنات نفسها.


فوه ... الكثير من التفاصيل ، لكنني أعتقد أننا غطينا كل ما أردت تغطيته. في الجزء الرابع سنتحدث عن تفاصيل vtables ذات المستوى vtables . لا تخطي ، لأن هذا ربما يكون الجزء الأكثر أهمية في هذه المقالة!


الجزء 4 - رمز تم إنشاؤه بواسطة المترجم


في هذه المرحلة من هذه المقالة ، تعلمنا كيف vtables typeinfo vtables و typeinfo مع الثنائيات الخاصة بنا وكيف يستخدمها المترجم. الآن سوف نفهم جزء العمل الذي يقوم به المترجم تلقائيًا.


الصانعين


منشئ أي فئة ، يتم إنشاء التعليمات البرمجية التالية:


  • استدعاء الوالدين يبني ، إن وجدت ؛
  • تحديد مؤشرات vtable ، إن وجدت ؛
  • تهيئة الأعضاء وفقًا لقائمة المُهيئات ؛
  • تنفيذ التعليمات البرمجية داخل أقواس المنشئ.

كل ما سبق يمكن أن يحدث بدون رمز واضح:


  • تبدأ برامج إنشاء الوالدين تلقائيًا افتراضيًا ما لم ينص على خلاف ذلك ؛
  • تتم تهيئة الأعضاء افتراضيًا إذا لم يكن لديهم قيمة أو إدخالات افتراضية في قائمة المهيئ ؛
  • يمكن إنشاء علامة المنشئ بالكامل = افتراضي ؛
  • فقط مهمة vtable مخفية دائمًا.

هنا مثال:


 #include <iostream> #include <string> using namespace std; class Parent { public: Parent() { Foo(); } virtual ~Parent() = default; virtual void Foo() { cout << "Parent" << endl; } int i = 0; }; class Child : public Parent { public: Child() : j(1) { Foo(); } void Foo() override { cout << "Child" << endl; } int j; }; class Grandchild : public Child { public: Grandchild() { Foo(); s = "hello"; } void Foo() override { cout << "Grandchild" << endl; } string s; }; int main() { Grandchild g; } 

لنكتب رمزًا زائفًا لمنشئ كل فصل:


أصلطفلحفيد
1. vtable = vtable الوالد ؛1. يدعو المنشئ الافتراضي الأم ؛1. يدعو المنشئ الافتراضي الطفل ؛
2. أنا = 0 ؛2. vtable = طفل vtable ؛2. vtable = vtable حفيد.
3. يدعو Foo () ؛3. ي = 1 ؛3. ، يدعو المنشئ الافتراضي ؛
4. يدعو Foo () ؛4. يدعو Foo () ؛
5. يستدعي العامل = s ؛

بالنظر إلى هذا ، فليس من المستغرب أنه في سياق مُنشئ الفصل ، يشير vtable إلى vtable لهذه الفئة نفسها ، وليس إلى فئتها المحددة. هذا يعني أنه يتم حل المكالمات الافتراضية كما لو لم يكن هناك ورثة متاحون. وبالتالي ، فإن الخاتمة


 Parent Child Grandchild 

ماذا عن الوظائف الافتراضية الخالصة؟ إذا لم يتم تنفيذها (نعم ، فيمكنك تنفيذ وظائف افتراضية بحتة ، ولكن لماذا تحتاج إلى ذلك؟) ، من المحتمل أنك (ونأمل) الانتقال مباشرة إلى segfault. بعض المجمعين يهملون الخطأ ، وهو رائع.


تالفة


كما يمكنك أن تتخيل ، فإن المدمرات تتصرف بنفس طريقة سلوك المنشئات ، فقط بالترتيب العكسي.


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


يلقي ضمنا


كما رأينا في الجزأين الثاني والثالث ، فإن المؤشر إلى كائن فرعي لا يساوي بالضرورة المؤشر الأصلي لنفس المثيل (كما في حالة الوراثة المتعددة).


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


المصبوب الديناميكي (RTTI)


تستخدم القوالب الديناميكية جداول typeinfo ، والتي درسناها في الجزء الأول. يفعلون ذلك في وقت التشغيل ، typeinfo إدخال typeinfo بمؤشر واحد قبل ما يشير إليه المؤشر vtable ، ويستخدمون الفصل من هناك للتحقق مما إذا كان المدلى بها ممكنًا.


هذا ما يفسر تكلفة dynamic_cast عند استخدامها بشكل متكرر.


مؤشرات الطريقة


أخطط لكتابة منشور كامل حول مؤشرات الطرق في المستقبل. قبل ذلك ، أود التأكيد على أن المؤشر إلى طريقة تشير إلى دالة ظاهرية سوف يستدعي فعليًا أسلوبًا تم تجاوزه (على عكس المؤشرات إلى وظائف ليست أعضاء).


 // TODO:  ,     

تحقق من نفسك!


يمكنك الآن أن تشرح لنفسك لماذا تتصرف جزء الكود التالي بالطريقة التي تتصرف بها:


 #include <iostream> using namespace std; class FooInterface { public: virtual ~FooInterface() = default; virtual void Foo() = 0; }; class BarInterface { public: virtual ~BarInterface() = default; virtual void Bar() = 0; }; class Concrete : public FooInterface, public BarInterface { public: void Foo() override { cout << "Foo()" << endl; } void Bar() override { cout << "Bar()" << endl; } }; int main() { Concrete c; c.Foo(); c.Bar(); FooInterface* foo = &c; foo->Foo(); BarInterface* bar = (BarInterface*)(foo); bar->Bar(); //  "Foo()" - WTF? } 

بهذا تنتهي مقالتي المكونة من أربعة أجزاء. أتمنى أن تتعلم شيئًا جديدًا ، مثلي تمامًا.

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


All Articles