Apa itu tabel tabel virtual?

Suatu kali di Slack, saya menemukan akronim baru untuk glosarium akronim C ++ saya: β€œVTT.” Godbolt :

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

"VTT" dalam konteks ini berarti "tabel tabel virtual". Ini adalah struktur data tambahan yang digunakan (dalam Itanium C ++ ABI) saat membuat beberapa kelas dasar yang diwarisi sendiri dari kelas basis virtual. VTT mengikuti aturan tata letak yang sama dengan tabel virtual (vtable) dan mengetik informasi (typeinfo), jadi jika Anda mendapatkan kesalahan di atas, Anda bisa mengganti secara mental "vtable" untuk "VTT" dan mulai debugging. (Kemungkinan besar, Anda meninggalkan fungsi kunci dari kelas tidak terdefinisi). Untuk melihat mengapa VTT, atau struktur serupa, diperlukan, mari kita mulai dengan dasar-dasarnya.

Urutan Desain untuk Warisan Non-Virtual


Ketika kita memiliki hierarki warisan, kelas dasar dibangun mulai dari yang paling dasar . Untuk membangun Charlie, pertama-tama kita harus membangun kelas orang tuanya MrsBucket dan MrBucket, secara rekursif, untuk membangun MrBucket, pertama-tama kita harus membangun kelas orang tuanya GrandmaJosephine dan GrandpaJoe.

Seperti ini:

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

Urutan Desain untuk Kelas Dasar Virtual


Tetapi warisan virtual membingungkan semua kartu! Dengan warisan virtual, kita dapat memiliki hierarki berbentuk berlian di mana dua kelas induk yang berbeda dapat berbagi leluhur yang sama.

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

Di bagian terakhir, setiap konstruktor bertanggung jawab untuk memanggil konstruktor dari kelas dasarnya. Tapi sekarang kita memiliki warisan virtual, dan konstruktor M dan F entah bagaimana harus tahu bahwa tidak perlu untuk membangun G, karena itu umum. Jika M dan F bertanggung jawab untuk membangun objek dasar dalam kasus ini, objek dasar umum akan dibangun dua kali, yang tidak terlalu baik.

Untuk bekerja dengan sub-objek warisan virtual, Itanium C ++ ABI membagi setiap konstruktor menjadi dua bagian: konstruktor objek dasar dan konstruktor objek penuh. Konstruktor objek dasar bertanggung jawab untuk membangun semua sub-objek warisan non-virtual (dan sub-objek mereka, dan menginstal vptr pada vtable mereka, dan menjalankan kode dalam kurung kurawal dalam kode C ++). Konstruktor objek lengkap, yang disebut setiap kali Anda membuat objek C ++ lengkap, bertanggung jawab untuk membangun semua sub-objek dari warisan virtual objek yang diturunkan dan kemudian melakukan sisanya.

Pertimbangkan perbedaan antara contoh ABCDE kami dari bagian sebelumnya dan contoh berikut:

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

Konstruktor objek lengkap E pertama memanggil konstruktor objek dasar dari sub-objek virtual A dan C; maka konstruktor dari objek pewarisan non-virtual B dan D disebut. B dan D tidak lagi bertanggung jawab untuk membangun A dan C, masing-masing.

Merancang tabel vtable


Misalkan kita memiliki kelas dengan beberapa metode virtual, misalnya, seperti ( 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()); } }; 

Ketika kita membangun Lion, kita mulai dengan membangun subobject Cat dasar. Konstruktor poke Panggilan kucing (). Pada titik ini, kami hanya memiliki satu objek Cat - kami belum menginisialisasi data anggota yang diperlukan untuk membuat objek Lion. Jika konstruktor Cat memanggil Lion :: poke (), ia dapat mencoba mengubah anggota std :: string roar yang belum diinisialisasi dan kami mendapatkan UB. Jadi, standar C ++ mengharuskan kita untuk melakukan ini di konstruktor Cat, panggilan ke metode virtual poke () harus memanggil Cat :: poke (), bukan Lion :: poke ()!

Tidak ada masalah. Kompiler hanya menyebabkan Cat :: Cat () (baik versi untuk objek dasar dan versi untuk objek lengkap) untuk memulai dengan mengatur vptr objek ke tabel objek Cat. Lion :: Lion () akan memanggil Cat :: Cat (), dan kemudian mengatur ulang vptr ke pointer ke vtable untuk objek Cat di dalam Lion, sebelum menjalankan kode dalam tanda kurung. Tidak masalah!

Penyeimbangan Warisan Virtual


Biarkan Kucing mewarisi dari Hewan. Kemudian, vtable untuk Cat tidak hanya menyimpan pointer fungsi untuk fungsi anggota virtual Cat, tetapi juga offset dari subobjek virtual Hewan di dalam Cat. ( Godbolt .)

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

Konstruktor Cat menanyakan data Animal :: anggota. Jika objek Cat ini adalah sub objek dasar objek Nermal, maka data anggotanya adalah pada offset 8, tepat di belakang vptr. Tetapi jika objek Cat adalah subobject yang mendasari objek Garfield, maka data anggota berada pada offset 16, di belakang vptr dan Garfield :: padding. Untuk mengatasinya, Itanium ABI menyimpan offset objek dasar virtual dalam tabel objek Cat. Tabel untuk Cat-in-Nermal mempertahankan fakta bahwa Animal, subobject Cat yang mendasarinya, disimpan pada offset 8; Tabel untuk Cat-in-Garfield mempertahankan fakta bahwa Animal, subobject Cat yang mendasarinya, disimpan pada offset 16.

Sekarang gabungkan ini dengan bagian sebelumnya. Compiler harus memastikan bahwa Cat :: Cat () (baik versi objek dasar dan versi objek lengkap) dimulai dengan menginstal vptr pada vtable untuk Cat-in-Nermal atau pada vtable untuk Cat-in-Garfield, tergantung pada jenisnya sebagian besar fasilitas turunan! Tetapi bagaimana cara kerjanya?

Konstruktor objek lengkap untuk objek yang paling diturunkan harus melakukan prapenghitungan tabel tabel mana yang ia inginkan untuk vptr dari sub objek objek yang akan direferensikan pada saat membangun objek, dan kemudian konstruktor objek lengkap untuk objek yang paling diturunkan harus meneruskan informasi ini ke konstruktor objek dasar sub objek objek. sebagai parameter tersembunyi! Mari kita lihat kode yang dihasilkan untuk 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 

Konstruktor objek dasar tidak hanya menerima parameter tersembunyi ini di% rdi, tetapi juga parameter VTT tersembunyi di% rsi! Konstruktor objek dasar memuat alamat dari (% rsi) dan menyimpan alamat dalam tabel objek Cat.

Siapa pun yang memanggil konstruktor objek Cat dasar bertanggung jawab untuk memprediksi alamat Cat :: Cat () mana yang harus ditulis dalam vptr dan untuk mengatur pointer di (% rsi) ke alamat itu.

Mengapa kita membutuhkan tingkat identitas lain?


Pertimbangkan konstruktor objek Nermal penuh.

 _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 

Mengapa _ZTC6Nermal0_3Cat + 24 terletak di bagian data dan alamatnya dilewatkan ke% rsi, alih-alih hanya meneruskan _ZTC6Nermal0_3Cat + 24 secara langsung?

 #   ? _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 

Ini karena kita dapat memiliki beberapa tingkat warisan! Pada setiap tingkat pewarisan, konstruktor objek dasar harus mengatur vptr dan kemudian, mungkin, meneruskan kontrol ke rantai ke konstruktor basis berikutnya, yang dapat mengatur vptr ke beberapa nilai lain. Ini menyiratkan daftar atau tabel pointer ke vtable.

Berikut ini adalah contoh nyata ( 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} {} }; 

Objek konstruktor dasar Grandparent harus mengatur vptr ke Grandparent - sesuatu yang lain, yang merupakan kelas yang paling diturunkan. Konstruktor objek dasar Induk harus terlebih dahulu memanggil Grandparent :: Grandparent () dengan% rsi yang sesuai, dan kemudian mengatur vptr ke Induk - sesuatu yang lain, yang merupakan kelas yang paling diturunkan. Cara untuk menerapkan ini untuk 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 

Anda dapat melihat di Godbolt bahwa konstruktor objek dasar dari kelas Induk pertama memanggil Grandparent :: Grandparent () dengan% rsi + 8, kemudian menetapkan vptr sendiri ke (% rsi). Jadi, di sini kita menggunakan fakta bahwa Gretel, dengan kata lain, dengan hati-hati meletakkan jalur remah roti yang diikuti oleh semua kelas dasar selama konstruksi.

VTT yang sama digunakan dalam destructor ( Godbolt ). Sejauh yang saya tahu, baris nol dari tabel VTT tidak pernah digunakan. Konstruktor Gretel memuat vtable untuk Gretel + 24 ke dalam vptr, tetapi ia tahu bahwa alamat ini statis, tidak perlu dimuat dari VTT. Saya berpikir bahwa baris nol tabel dipertahankan hanya karena alasan historis. (Dan tentu saja, kompiler tidak bisa begitu saja membuangnya, karena itu akan menjadi pelanggaran Itanium ABI dan tidak mungkin untuk menautkan ke kode lama yang menganut Itanium-ABI).

Itu saja, kami melihat tabel tabel virtual, atau VTT.

Informasi lebih lanjut


Anda dapat menemukan informasi VTT di tempat-tempat ini:

StackOverflow: β€œ Apa VTT untuk sebuah kelas? ”
" Catatan VTable pada Berbagai Warisan dalam GCC C ++ Compiler v4.0.1 " (Morgan Deters, 2005)
Bagian Itanium C ++ ABI , β€œVTT Order”

Akhirnya, saya harus mengulangi bahwa VTT adalah fitur dari Itanium C ++ ABI, dan digunakan di Linux, OSX, dll. MSVC ABI yang digunakan pada Windows tidak memiliki VTT, dan menggunakan mekanisme yang sama sekali berbeda untuk warisan virtual. Saya (sejauh ini) hampir tidak tahu apa-apa tentang MSVC ABI, tapi mungkin suatu hari saya akan mengetahui semuanya dan menulis posting tentang itu!

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


All Articles