Kita semua tahu apa fungsi virtual di C ++, tetapi bagaimana mereka diterapkan pada level yang dalam?
Bisakah vtable diubah atau bahkan diakses langsung saat runtime?
Apakah vtable ada untuk semua kelas, atau hanya yang memiliki setidaknya satu fungsi virtual?
Apakah kelas abstrak hanya memiliki NULL untuk penunjuk fungsi setidaknya satu entri?
Apakah memiliki satu fungsi virtual memperlambat seluruh kelas? Atau hanya panggilan ke fungsi yang bersifat virtual? Dan apakah kecepatan akan terpengaruh jika fungsi virtual benar-benar ditimpa atau tidak, atau apakah ini tidak berpengaruh selama itu virtual.
c++
polymorphism
virtual-functions
vtable
Brian R. Bondy
sumber
sumber
Inside the C++ Object Model
denganStanley B. Lippman
. (Bagian 4.2, halaman 124-131)Jawaban:
Bagaimana fungsi virtual diterapkan pada level yang dalam?
Dari "Fungsi Virtual di C ++" :
Bisakah vtable diubah atau bahkan diakses langsung saat runtime?
Secara universal, saya yakin jawabannya adalah "tidak". Anda bisa melakukan beberapa memory mangling untuk menemukan vtable tetapi Anda masih tidak tahu seperti apa fungsi signature untuk memanggilnya. Apa pun yang ingin Anda capai dengan kemampuan ini (yang didukung bahasa) harus dimungkinkan tanpa akses ke vtable secara langsung atau mengubahnya saat runtime. Perhatikan juga, spesifikasi bahasa C ++ tidak menentukan bahwa vtables diperlukan - namun begitulah cara sebagian besar compiler mengimplementasikan fungsi virtual.
Apakah vtable ada untuk semua objek, atau hanya yang memiliki setidaknya satu fungsi virtual?
Saya percaya jawabannya di sini adalah "itu tergantung pada implementasinya" karena spesifikasi tidak memerlukan vtables di tempat pertama. Namun, dalam praktiknya, saya percaya semua kompiler modern hanya membuat vtable jika sebuah kelas memiliki setidaknya 1 fungsi virtual. Ada overhead ruang yang terkait dengan vtable dan overhead waktu yang terkait dengan pemanggilan fungsi virtual vs fungsi non-virtual.
Apakah kelas abstrak hanya memiliki NULL untuk penunjuk fungsi setidaknya satu entri?
Jawabannya tidak ditentukan oleh spesifikasi bahasa sehingga tergantung pada implementasinya. Memanggil fungsi virtual murni menghasilkan perilaku tidak terdefinisi jika tidak ditentukan (yang biasanya tidak) (ISO / IEC 14882: 2003 10.4-2). Dalam praktiknya, ia mengalokasikan slot di vtable untuk fungsi tersebut tetapi tidak menetapkan alamat untuk itu. Hal ini membuat vtable tidak lengkap yang membutuhkan kelas turunan untuk mengimplementasikan fungsi dan menyelesaikan vtable. Beberapa implementasi hanya menempatkan pointer NULL di entri vtable; implementasi lain menempatkan pointer ke metode dummy yang melakukan sesuatu yang mirip dengan sebuah pernyataan.
Perhatikan bahwa kelas abstrak dapat mendefinisikan implementasi untuk fungsi virtual murni, tetapi fungsi itu hanya dapat dipanggil dengan sintaks id yang memenuhi syarat (yaitu, sepenuhnya menentukan kelas dalam nama metode, mirip dengan memanggil metode kelas dasar dari kelas turunan). Ini dilakukan untuk menyediakan implementasi default yang mudah digunakan, sambil tetap mensyaratkan bahwa kelas turunan menyediakan penggantian.
Apakah memiliki satu fungsi virtual memperlambat seluruh kelas atau hanya panggilan ke fungsi virtual?
Ini semakin mendekati pengetahuan saya, jadi seseorang tolong bantu saya jika saya salah!
Saya percaya bahwa hanya fungsi yang virtual di kelas yang mengalami kinerja waktu yang terkait dengan pemanggilan fungsi virtual vs. fungsi non-virtual. Ruang overhead untuk kelas ada di sana. Perhatikan bahwa jika ada vtabel, hanya ada 1 per kelas , bukan satu per objek .
Apakah kecepatan terpengaruh jika fungsi virtual benar-benar diganti atau tidak, atau apakah ini tidak berpengaruh selama itu virtual?
Saya tidak percaya waktu pelaksanaan fungsi virtual yang diganti berkurang dibandingkan dengan memanggil fungsi virtual dasar. Namun, ada overhead ruang tambahan untuk kelas yang terkait dengan mendefinisikan vtabel lain untuk kelas turunan vs kelas dasar.
Sumber daya tambahan:
http://www.codersource.net/published/view/325/virtual_functions_in.aspx (melalui mesin jalan kembali)
http://en.wikipedia.org/wiki/Virtual_table
http://www.codesourcery.com/public/ cxx-abi / abi.html # vtable
sumber
Tidak portabel, tetapi jika Anda tidak keberatan dengan trik kotor, tentu!
Di sebagian besar kompiler yang pernah saya lihat, vtbl * adalah 4 byte pertama dari objek, dan konten vtbl hanyalah sebuah array pointer anggota di sana (umumnya dalam urutan mereka dideklarasikan, dengan kelas dasar yang pertama). Tentu saja ada tata letak lain yang memungkinkan, tetapi itulah yang biasanya saya amati.
Sekarang untuk menarik beberapa kejahatan ...
Mengubah kelas saat runtime:
Mengganti metode untuk semua instance (monkeypatching kelas)
Yang ini sedikit lebih rumit, karena vtbl itu sendiri mungkin ada dalam memori hanya-baca.
Yang terakhir ini cenderung membuat pemeriksa virus dan tautan terbangun dan memperhatikan, karena manipulasi mprotect. Dalam proses menggunakan bit NX, ini mungkin gagal.
sumber
Apakah memiliki satu fungsi virtual memperlambat seluruh kelas?
Memiliki fungsi virtual memperlambat seluruh kelas sejauh satu item data lagi harus diinisialisasi, disalin,… saat berhadapan dengan objek dari kelas semacam itu. Untuk kelas dengan setengah lusin anggota atau lebih, perbedaannya harus diabaikan. Untuk kelas yang hanya berisi satu
char
anggota, atau tanpa anggota sama sekali, perbedaannya mungkin terlihat.Selain itu, penting untuk diperhatikan bahwa tidak setiap panggilan ke fungsi virtual adalah panggilan fungsi virtual. Jika Anda memiliki objek dengan tipe yang diketahui, kompilator dapat memancarkan kode untuk pemanggilan fungsi normal, dan bahkan dapat menyebariskan fungsi tersebut jika diinginkan. Hanya ketika Anda melakukan panggilan polimorfik, melalui pointer atau referensi yang mungkin menunjuk ke objek kelas dasar atau objek dari beberapa kelas turunan, Anda memerlukan tipuan vtable dan membayarnya dalam hal kinerja.
Langkah-langkah yang harus diambil perangkat keras pada dasarnya sama, tidak peduli apakah fungsinya ditimpa atau tidak. Alamat vtable dibaca dari objek, penunjuk fungsi diambil dari slot yang sesuai, dan fungsi dipanggil oleh penunjuk. Dalam hal kinerja aktual, prediksi cabang mungkin berdampak. Jadi misalnya, jika sebagian besar objek Anda merujuk ke implementasi yang sama dari fungsi virtual tertentu, maka ada beberapa kemungkinan bahwa prediktor cabang akan memprediksi dengan benar fungsi mana yang akan dipanggil bahkan sebelum pointer diambil. Tapi tidak masalah fungsi mana yang umum: bisa jadi sebagian besar objek didelegasikan ke kasus dasar yang tidak ditimpa, atau sebagian besar objek milik subkelas yang sama dan oleh karena itu mendelegasikan ke kasus yang ditimpa sama.
bagaimana penerapannya di tingkat yang dalam?
Saya suka ide jheriko untuk mendemonstrasikan ini menggunakan implementasi tiruan. Tetapi saya akan menggunakan C untuk mengimplementasikan sesuatu yang mirip dengan kode di atas, sehingga level rendah lebih mudah dilihat.
kelas orang tua Foo
kelas turunan Bar
fungsi f melakukan panggilan fungsi virtual
Jadi Anda bisa lihat, vtable hanyalah blok statis dalam memori, sebagian besar berisi pointer fungsi. Setiap objek dari kelas polimorfik akan mengarah ke vtabel yang sesuai dengan tipe dinamisnya. Ini juga membuat koneksi antara RTTI dan fungsi virtual lebih jelas: Anda dapat memeriksa tipe kelas apa hanya dengan melihat apa yang ditunjuk oleh vtable. Hal di atas disederhanakan dalam banyak hal, seperti misalnya pewarisan ganda, tetapi konsep umumnya masuk akal.
Jika
arg
bertipeFoo*
dan Anda ambilarg->vtable
, tetapi sebenarnya merupakan objek bertipeBar
, maka Anda masih mendapatkan alamat yang benar darivtable
. Itu karenavtable
selalu merupakan elemen pertama di alamat objek, tidak peduli apakah itu dipanggilvtable
ataubase.vtable
dalam ekspresi yang diketik dengan benar.sumber
Biasanya dengan VTable, serangkaian pointer ke fungsi.
sumber
Jawaban ini telah dimasukkan ke dalam jawaban Wiki Komunitas
Jawaban untuk itu adalah tidak ditentukan - memanggil fungsi virtual murni menghasilkan perilaku yang tidak ditentukan jika tidak ditentukan (yang biasanya tidak) (ISO / IEC 14882: 2003 10.4-2). Beberapa implementasi hanya menempatkan pointer NULL di entri vtable; implementasi lain menempatkan pointer ke metode dummy yang melakukan sesuatu yang mirip dengan sebuah pernyataan.
Perhatikan bahwa kelas abstrak dapat mendefinisikan implementasi untuk fungsi virtual murni, tetapi fungsi itu hanya dapat dipanggil dengan sintaks id yang memenuhi syarat (yaitu, sepenuhnya menentukan kelas dalam nama metode, mirip dengan memanggil metode kelas dasar dari kelas turunan). Ini dilakukan untuk menyediakan implementasi default yang mudah digunakan, sambil tetap mensyaratkan bahwa kelas turunan menyediakan penggantian.
sumber
Anda dapat membuat ulang fungsionalitas fungsi virtual di C ++ menggunakan pointer fungsi sebagai anggota kelas dan fungsi statis sebagai implementasinya, atau menggunakan pointer ke fungsi anggota dan fungsi anggota untuk implementasi. Hanya ada keuntungan notasi antara kedua metode ... pada kenyataannya pemanggilan fungsi virtual hanyalah kenyamanan notasi itu sendiri. Faktanya, pewarisan hanyalah kenyamanan notasional ... itu semua dapat diterapkan tanpa menggunakan fitur bahasa untuk pewarisan. :)
Di bawah ini adalah omong kosong yang belum teruji, mungkin kode buggy, tapi semoga bisa menunjukkan idenya.
misalnya
sumber
void(*)(Foo*) MyFunc;
apakah ini beberapa sintaks Java?Saya akan mencoba membuatnya sederhana :)
Kita semua tahu apa fungsi virtual di C ++, tetapi bagaimana mereka diterapkan pada level yang dalam?
Ini adalah larik dengan pointer ke fungsi, yang merupakan implementasi dari fungsi virtual tertentu. Indeks dalam larik ini mewakili indeks tertentu dari fungsi virtual yang ditentukan untuk sebuah kelas. Ini termasuk fungsi virtual murni.
Ketika kelas polimorfik diturunkan dari kelas polimorfik lain, kita mungkin mengalami situasi berikut:
Bisakah vtable diubah atau bahkan diakses langsung saat runtime?
Bukan cara standar - tidak ada API untuk mengaksesnya. Penyusun mungkin memiliki beberapa ekstensi atau API pribadi untuk mengaksesnya, tetapi itu mungkin hanya ekstensi.
Apakah vtable ada untuk semua kelas, atau hanya yang memiliki setidaknya satu fungsi virtual?
Hanya mereka yang memiliki setidaknya satu fungsi virtual (bahkan destruktor) atau mendapatkan setidaknya satu kelas yang memiliki vtabelnya ("bersifat polimorfik").
Apakah kelas abstrak hanya memiliki NULL untuk penunjuk fungsi setidaknya satu entri?
Itu adalah implementasi yang mungkin, tetapi tidak dipraktikkan. Sebaliknya biasanya ada fungsi yang mencetak sesuatu seperti "fungsi virtual murni yang disebut" dan melakukannya
abort()
. Panggilan ke itu mungkin terjadi jika Anda mencoba memanggil metode abstrak di konstruktor atau destruktor.Apakah memiliki satu fungsi virtual memperlambat seluruh kelas? Atau hanya panggilan ke fungsi yang bersifat virtual? Dan apakah kecepatan akan terpengaruh jika fungsi virtual benar-benar ditimpa atau tidak, atau apakah ini tidak berpengaruh selama itu virtual.
Perlambatan hanya bergantung pada apakah panggilan diselesaikan sebagai panggilan langsung atau panggilan virtual. Dan tidak ada lagi yang penting. :)
Jika Anda memanggil fungsi virtual melalui pointer atau referensi ke suatu objek, maka itu akan selalu diimplementasikan sebagai panggilan virtual - karena compiler tidak akan pernah tahu jenis objek apa yang akan ditugaskan ke pointer ini dalam runtime, dan apakah itu dari sebuah kelas di mana metode ini diganti atau tidak. Hanya dalam dua kasus, compiler dapat menyelesaikan panggilan ke fungsi virtual sebagai panggilan langsung:
final
di kelas tempat Anda memiliki pointer atau referensi yang Anda gunakan untuk memanggilnya ( hanya di C ++ 11 ). Dalam hal ini kompilator mengetahui bahwa metode ini tidak dapat menjalani penggantian lebih lanjut dan hanya dapat menjadi metode dari kelas ini.Perhatikan bahwa panggilan virtual hanya memiliki overhead dereferencing dua petunjuk. Menggunakan RTTI (meskipun hanya tersedia untuk kelas polimorfik) lebih lambat daripada memanggil metode virtual, jika Anda menemukan kasus untuk mengimplementasikan hal yang sama dengan dua cara. Misalnya, menentukan
virtual bool HasHoof() { return false; }
dan mengganti hanya sebagaimanabool Horse::HasHoof() { return true; }
akan memberi Anda kemampuan untuk meneleponif (anim->HasHoof())
yang akan lebih cepat daripada mencobaif(dynamic_cast<Horse*>(anim))
. Ini karenadynamic_cast
harus berjalan melalui hierarki kelas dalam beberapa kasus bahkan secara rekursif untuk melihat apakah ada jalur yang dibangun dari tipe pointer aktual dan tipe kelas yang diinginkan. Sementara panggilan virtual selalu sama - dereferensi dua petunjuk.sumber
Berikut ini adalah runnable implementasi manual tabel virtual di C ++ modern. Ini memiliki semantik yang terdefinisi dengan baik, tidak ada peretasan dan tidak
void*
.Catatan:
.*
dan->*
merupakan operator berbeda dari*
dan->
. Pointer fungsi anggota bekerja secara berbeda.sumber
Setiap objek memiliki penunjuk vtable yang menunjuk ke array fungsi anggota.
sumber
Sesuatu yang tidak disebutkan di sini dalam semua jawaban ini adalah dalam kasus warisan berganda, di mana semua kelas dasar memiliki metode virtual. Kelas mewarisi memiliki beberapa pointer ke vmt. Hasilnya adalah ukuran setiap instance dari objek tersebut lebih besar. Semua orang tahu bahwa kelas dengan metode virtual memiliki 4 byte ekstra untuk vmt, tetapi dalam kasus warisan berganda untuk setiap kelas dasar yang memiliki metode virtual dikalikan 4. 4 adalah ukuran pointer.
sumber
Jawaban Burly benar di sini kecuali untuk pertanyaannya:
Apakah kelas abstrak hanya memiliki NULL untuk penunjuk fungsi setidaknya satu entri?
Jawabannya adalah tidak ada tabel virtual yang dibuat sama sekali untuk kelas abstrak. Tidak diperlukan karena tidak ada objek kelas ini yang dapat dibuat!
Dengan kata lain jika kita memiliki:
Pointer vtbl yang diakses melalui pB akan menjadi vtbl kelas D. Ini persis bagaimana polimorfisme diimplementasikan. Artinya, bagaimana metode D diakses melalui pB. Tidak perlu vtbl untuk kelas B.
Menanggapi komentar Mike di bawah ini ...
Jika kelas B dalam uraian saya memiliki metode virtual foo () yang tidak diganti oleh D dan bilah metode virtual () yang diganti, maka vtbl D akan memiliki penunjuk ke foo () B dan ke barnya sendiri () . Masih belum ada vtbl yang dibuat untuk B.
sumber
B
harus diperlukan. Hanya karena beberapa metodenya memiliki implementasi (default) tidak berarti mereka harus disimpan dalam vtable. Tapi saya baru saja menjalankan kode Anda (modulo beberapa perbaikan untuk membuatnya dikompilasi) melaluigcc -S
diikuti olehc++filt
dan jelas ada vtable untukB
disertakan di sana. Saya rasa itu mungkin karena vtable juga menyimpan data RTTI seperti nama kelas dan warisan. Mungkin diperlukan untuk adynamic_cast<B*>
. Bahkan-fno-rtti
tidak membuat vtable hilang. Denganclang -O3
bukannyagcc
tiba-tiba hilang.bukti konsep yang sangat lucu yang saya buat sedikit lebih awal (untuk melihat apakah urutan pewarisan itu penting); beri tahu saya jika implementasi C ++ Anda benar-benar menolaknya (versi gcc saya hanya memberikan peringatan untuk menetapkan struct anonim, tapi itu bug), saya penasaran.
CCPolite.h :
CCPolite_constructor.h :
main.c :
keluaran:
perhatikan karena saya tidak pernah mengalokasikan objek palsu saya, tidak perlu melakukan penghancuran apa pun; destructors secara otomatis diletakkan di akhir lingkup objek yang dialokasikan secara dinamis untuk mendapatkan kembali memori literal objek itu sendiri dan penunjuk vtable.
sumber