Meskipun ini tidak wajib dalam standar C ++, tampaknya cara GCC misalnya, mengimplementasikan kelas induk, termasuk yang abstrak murni, adalah dengan memasukkan pointer ke tabel-v untuk kelas abstrak itu di setiap instance dari kelas yang bersangkutan .
Tentu saja ini menggembungkan ukuran setiap instance dari kelas ini dengan sebuah pointer untuk setiap kelas induk yang dimilikinya.
Tapi saya perhatikan bahwa banyak kelas dan struct C # memiliki banyak antarmuka orang tua, yang pada dasarnya adalah kelas abstrak murni. Saya akan terkejut jika setiap contoh mengatakan Decimal
, membengkak dengan 6 pointer ke semua itu berbagai antarmuka.
Jadi jika C # melakukan antarmuka yang berbeda, bagaimana cara melakukannya, setidaknya dalam implementasi yang khas (saya mengerti standar itu sendiri mungkin tidak mendefinisikan implementasi seperti itu)? Dan apakah implementasi C ++ memiliki cara untuk menghindari ukuran objek mengasapi ketika menambahkan orang tua virtual murni ke kelas?
sumber
IComparer
denganCompare
g++-7 -fdump-class-hierarchy
output.Jawaban:
Dalam implementasi C # dan Java, objek biasanya memiliki satu pointer ke kelasnya. Ini dimungkinkan karena mereka adalah bahasa warisan tunggal. Struktur kelas kemudian berisi vtable untuk hierarki warisan tunggal. Tetapi memanggil metode antarmuka memiliki semua masalah warisan ganda juga. Ini biasanya diselesaikan dengan menempatkan vtables tambahan untuk semua antarmuka yang diimplementasikan ke dalam struktur kelas. Ini menghemat ruang dibandingkan dengan implementasi virtual inheritance pada C ++, tetapi membuat pengiriman metode antarmuka lebih rumit - yang sebagian dapat dikompensasi dengan caching.
Misalnya dalam OpenJDK JVM, setiap kelas berisi array vtable untuk semua antarmuka yang diimplementasikan (antarmuka vtable disebut itable ). Ketika suatu metode antarmuka dipanggil, array ini dicari secara linear untuk mengetahui kemungkinan antarmuka itu, maka metode tersebut dapat dikirim melalui yang dapat dijalankan. Caching digunakan sehingga setiap situs panggilan mengingat hasil dari pengiriman metode, jadi pencarian ini hanya harus diulang ketika tipe objek konkret berubah. Kodesemu untuk pengiriman metode:
(Bandingkan kode asli di interpreter OpenJDK HotSpot atau kompiler x86 .)
C # (atau lebih tepatnya, CLR) menggunakan pendekatan terkait. Namun, di sini itables tidak berisi pointer ke metode, tetapi adalah slot map: mereka menunjuk ke entri dalam tabel utama kelas. Seperti halnya Java, harus mencari yang benar benar hanya skenario terburuk, dan diharapkan bahwa caching di situs panggilan dapat menghindari pencarian ini hampir selalu. CLR menggunakan teknik yang disebut Virtual Stub Dispatch untuk menambal kode mesin yang dikompilasi JIT dengan strategi caching yang berbeda. Kodesemu:
Perbedaan utama dengan pseudocode OpenJDK adalah bahwa dalam OpenJDK setiap kelas memiliki array dari semua antarmuka yang diimplementasikan secara langsung atau tidak langsung, sedangkan CLR hanya menyimpan array peta slot untuk antarmuka yang diimplementasikan secara langsung di kelas tersebut. Karena itu, kita perlu menjalankan hierarki warisan ke atas hingga peta slot ditemukan. Untuk hierarki warisan yang dalam, ini menghasilkan penghematan ruang. Ini sangat relevan dalam CLR karena cara bagaimana generik diimplementasikan: untuk spesialisasi generik, struktur kelas disalin dan metode dalam tabel utama dapat digantikan oleh spesialisasi. Peta slot terus menunjuk pada entri vtable yang benar dan karenanya dapat dibagikan di antara semua spesialisasi generik suatu kelas.
Sebagai catatan akhir, ada lebih banyak kemungkinan untuk mengimplementasikan pengiriman antarmuka. Alih-alih menempatkan pointer vtable / itable di objek atau dalam struktur kelas, kita bisa menggunakan pointer lemak ke objek, yang pada dasarnya adalah
(Object*, VTable*)
sepasang. Kekurangannya adalah ini menggandakan ukuran pointer dan bahwa upcast (dari tipe beton ke tipe antarmuka) tidak gratis. Tetapi lebih fleksibel, memiliki sedikit tipuan, dan juga berarti bahwa antarmuka dapat diimplementasikan secara eksternal dari suatu kelas. Pendekatan terkait digunakan oleh antarmuka Go, ciri-ciri Rust, dan typeclasses Haskell.Referensi dan bacaan lebih lanjut:
sumber
callvirt
AKACEE_CALLVIRT
di CoreCLR adalah instruksi CIL yang menangani metode antarmuka panggilan, jika ada yang ingin membaca lebih lanjut tentang bagaimana runtime menangani pengaturan ini.call
opcode digunakan untukstatic
metode, yang menarikcallvirt
digunakan bahkan jika kelasnyasealed
.Jika dengan 'kelas induk' yang Anda maksud adalah 'kelas dasar' maka ini bukan kasus di gcc (atau saya harapkan dalam kompiler lain).
Dalam kasus C berasal dari B berasal dari A di mana A adalah kelas polimorfik, turunan C akan memiliki tepat satu vtable.
Compiler memiliki semua informasi yang diperlukan untuk menggabungkan data dalam A's vtable ke B's dan B's ke C's.
Berikut ini sebuah contoh: https://godbolt.org/g/sfdtNh
Anda akan melihat bahwa hanya ada satu inisialisasi dari vtable.
Saya telah menyalin output rakitan untuk fungsi utama di sini dengan anotasi:
Sumber lengkap untuk referensi:
sumber
class Derived : public FirstBase, public SecondBase
maka mungkin ada dua vtable. Anda dapat menjalankang++ -fdump-class-hierarchy
untuk melihat tata letak kelas (juga ditampilkan di posting blog saya yang tertaut). Godbolt kemudian menunjukkan kenaikan pointer tambahan sebelum panggilan untuk memilih tabel ke-2.