Saya ingin tahu apa itu " kelas dasar virtual " dan apa artinya.
Izinkan saya menunjukkan sebuah contoh:
class Foo
{
public:
void DoSomething() { /* ... */ }
};
class Bar : public virtual Foo
{
public:
void DoSpecific() { /* ... */ }
};
c++
virtual-inheritance
popopome
sumber
sumber
Jawaban:
Kelas dasar virtual, yang digunakan dalam warisan virtual, adalah cara untuk mencegah beberapa "instance" dari kelas yang diberikan muncul dalam hierarki warisan ketika menggunakan banyak pewarisan.
Pertimbangkan skenario berikut:
Hirarki kelas di atas menghasilkan "berlian menakutkan" yang terlihat seperti ini:
Sebuah instance dari D akan terdiri dari B, yang termasuk A, dan C yang juga termasuk A. Jadi Anda memiliki dua "instance" (karena ingin ekspresi yang lebih baik) dari A.
Ketika Anda memiliki skenario ini, Anda memiliki kemungkinan ambiguitas. Apa yang terjadi ketika Anda melakukan ini:
Warisan virtual ada untuk menyelesaikan masalah ini. Saat Anda menentukan virtual saat mewarisi kelas Anda, Anda memberi tahu kompiler bahwa Anda hanya menginginkan satu instance.
Ini berarti bahwa hanya ada satu "instance" dari A yang termasuk dalam hierarki. Karenanya
Ini adalah ringkasan mini. Untuk informasi lebih lanjut, baca ini dan ini . Contoh yang baik juga tersedia di sini .
sumber
virtual
, maka tata letak objek terlihat seperti berlian; dan jika kita tidak menggunakanvirtual
maka tata letak objek terlihat seperti struktur pohon yang berisi duaA
sTentang tata letak memori
Sebagai catatan, masalah dengan Dreaded Diamond adalah bahwa kelas dasar hadir beberapa kali. Jadi dengan warisan reguler, Anda yakin telah:
Namun dalam tata letak memori, Anda memiliki:
Ini menjelaskan mengapa saat menelepon
D::foo()
, Anda memiliki masalah ambiguitas. Tetapi masalah sebenarnya muncul ketika Anda ingin menggunakan variabel anggotaA
. Sebagai contoh, katakanlah kita memiliki:Ketika Anda akan mencoba mengakses
m_iValue
dariD
, kompiler akan memprotes, karena dalam hierarki, ia akan melihat duam_iValue
, bukan satu. Dan jika Anda memodifikasi satu, katakanlah,B::m_iValue
(yang merupakanA::m_iValue
induk dariB
),C::m_iValue
tidak akan dimodifikasi (itu adalahA::m_iValue
induk dariC
).Di sinilah warisan virtual berguna, karena dengan itu, Anda akan kembali ke tata letak berlian yang sebenarnya, dengan tidak hanya satu
foo()
metode saja, tetapi juga satu dan hanya satum_iValue
.Apa yang bisa salah?
Membayangkan:
A
memiliki beberapa fitur dasar.B
menambah semacam array data yang keren (misalnya)C
menambah beberapa fitur keren seperti pola pengamat (misalnya, aktifm_iValue
).D
mewarisi dariB
danC
, dan dengan demikian dariA
.Dengan warisan normal, pengubahan
m_iValue
dariD
adalah ambigu dan ini harus diselesaikan. Bahkan jika ada, ada duam_iValues
di dalamD
, jadi Anda sebaiknya mengingatnya dan memperbarui keduanya sekaligus.Dengan warisan virtual, memodifikasi
m_iValue
dariD
adalah ok ... Tapi ... Katakanlah Anda milikiD
. MelaluiC
antarmuka, Anda memasang pengamat. Dan melaluiB
antarmuka, Anda memperbarui array keren, yang memiliki efek samping mengubah langsungm_iValue
...Karena perubahan
m_iValue
dilakukan secara langsung (tanpa menggunakan metode pengakses virtual), pengamat "mendengarkan" melaluiC
tidak akan dipanggil, karena kode yang menerapkan mendengarkan dalamC
, danB
tidak tahu tentang itu ...Kesimpulan
Jika Anda memiliki berlian dalam hierarki Anda, itu berarti Anda memiliki kemungkinan 95% untuk melakukan sesuatu yang salah dengan hierarki tersebut.
sumber
Menjelaskan multiple-inheritance dengan basis virtual membutuhkan pengetahuan tentang model objek C ++. Dan menjelaskan topiknya dengan jelas paling baik dilakukan dalam sebuah artikel dan bukan dalam kotak komentar.
Penjelasan terbaik dan dapat dibaca yang saya temukan yang menyelesaikan semua keraguan saya pada subjek ini adalah artikel ini: http://www.phpcompiler.org/articles/virtualinheritance.html
Anda benar-benar tidak perlu membaca apa pun tentang topik (kecuali jika Anda adalah penulis kompiler) setelah membaca itu ...
sumber
Saya pikir Anda membingungkan dua hal yang sangat berbeda. Warisan virtual tidak sama dengan kelas abstrak. Warisan virtual mengubah perilaku panggilan fungsi; kadang-kadang ia menyelesaikan panggilan fungsi yang sebaliknya akan ambigu, kadang-kadang ia menolak penanganan panggilan fungsi ke kelas selain yang diharapkan dalam warisan non-virtual.
sumber
Saya ingin menambahkan klarifikasi jenis OJ.
Warisan virtual tidak datang tanpa harga. Seperti halnya semua hal virtual, Anda mendapatkan kinerja yang baik. Ada cara mengatasi hit kinerja ini yang mungkin kurang elegan.
Alih-alih memecahkan berlian dengan menurunkan secara virtual, Anda dapat menambahkan lapisan lain ke berlian, untuk mendapatkan sesuatu seperti ini:
Tidak ada kelas yang mewarisi secara virtual, semua mewarisi secara publik. Kelas D21 dan D22 kemudian akan menyembunyikan fungsi virtual f () yang ambigu untuk DD, mungkin dengan mendeklarasikan fungsi privat. Mereka masing-masing mendefinisikan fungsi wrapper, masing-masing f1 () dan f2 (), masing-masing memanggil kelas-lokal (pribadi) f (), sehingga menyelesaikan konflik. Panggilan kelas DD f1 () jika ingin D11 :: f () dan f2 () jika ingin D12 :: f (). Jika Anda mendefinisikan pembungkus inline Anda mungkin akan mendapatkan sekitar nol overhead.
Tentu saja, jika Anda dapat mengubah D11 dan D12 maka Anda dapat melakukan trik yang sama di dalam kelas-kelas ini, tetapi seringkali itu tidak terjadi.
sumber
Selain apa yang telah dikatakan tentang multiple and virtual inheritance (s), ada artikel yang sangat menarik di Dr Dobb's Journal: Multiple Warisan Dianggap Berguna
sumber
Anda sedikit membingungkan. Saya tidak tahu jika Anda mencampur beberapa konsep.
Anda tidak memiliki kelas dasar virtual di OP Anda. Anda hanya memiliki kelas dasar.
Anda melakukan warisan virtual. Ini biasanya digunakan dalam multiple inheritance sehingga beberapa kelas turunan menggunakan anggota kelas dasar tanpa mereproduksi mereka.
Kelas dasar dengan fungsi virtual murni tidak dapat dipakai. ini membutuhkan sintaksis yang dimiliki Paulus. Ini biasanya digunakan sehingga kelas turunan harus mendefinisikan fungsi-fungsi tersebut.
Saya tidak ingin menjelaskan lagi tentang ini karena saya tidak sepenuhnya mendapatkan apa yang Anda minta.
sumber
Ini berarti panggilan ke fungsi virtual akan diteruskan ke kelas "benar".
C ++ FAQ Lite FTW.
Singkatnya, ini sering digunakan dalam skenario multiple-inheritance, di mana hierarki "berlian" terbentuk. Warisan virtual kemudian akan memecah ambiguitas yang dibuat di kelas bawah, ketika Anda memanggil fungsi di kelas itu dan fungsi harus diselesaikan ke kelas D1 atau D2 di atas kelas bawah itu. Lihat item FAQ untuk diagram dan detailnya.
Ini juga digunakan dalam delegasi saudari , fitur yang kuat (meskipun tidak untuk yang lemah hati). Lihat ini FAQ .
Juga lihat Butir 40 dalam Efektif C ++ edisi ke-3 (43 dalam edisi ke-2).
sumber
Contoh penggunaan runnable warisan berlian
Contoh ini menunjukkan bagaimana menggunakan kelas dasar virtual dalam skenario tipikal: untuk menyelesaikan warisan berlian.
sumber
assert(A::aDefault == 0);
dari fungsi utama memberi saya kesalahan kompilasi:aDefault is not a member of A
menggunakan gcc 5.4.0. Apa yang harus dilakukan?Kelas virtual tidak sama dengan warisan virtual. Kelas virtual yang tidak dapat Anda instantiate, virtual inheritance adalah sesuatu yang sepenuhnya berbeda.
Wikipedia menggambarkannya lebih baik daripada yang saya bisa. http://en.wikipedia.org/wiki/Virtual_inheritance
sumber
Warisan Biasa
Dengan 3 level non-diamond non-virtual-inheritance inheritance, ketika Anda membuat instance objek paling baru, baru dipanggil dan ukuran yang diperlukan untuk objek diselesaikan dari tipe kelas oleh kompiler dan diteruskan ke yang baru.
baru memiliki tanda tangan:
Dan membuat panggilan ke
malloc
, mengembalikan pointer kosongIni kemudian diteruskan ke konstruktor dari objek yang paling diturunkan, yang akan segera memanggil konstruktor tengah dan kemudian konstruktor tengah akan segera memanggil konstruktor dasar. Basis kemudian menyimpan pointer ke tabel virtualnya di awal objek dan kemudian atributnya setelah itu. Ini kemudian kembali ke konstruktor tengah yang akan menyimpan pointer tabel virtualnya di lokasi yang sama dan kemudian atributnya setelah atribut yang akan disimpan oleh konstruktor dasar. Ini mengembalikan ke konstruktor yang paling diturunkan, yang menyimpan pointer ke tabel virtualnya di lokasi yang sama dan kemudian atributnya setelah atribut yang akan disimpan oleh konstruktor tengah.
Karena pointer tabel virtual ditimpa, pointer tabel virtual akhirnya selalu menjadi salah satu kelas yang paling diturunkan. Virtualness merambat ke kelas yang paling diturunkan jadi jika suatu fungsi virtual di kelas menengah, itu akan menjadi virtual di kelas yang paling diturunkan tetapi bukan kelas dasar. Jika Anda secara polimorfik membuat instance dari kelas yang paling diturunkan ke pointer ke kelas dasar maka kompiler tidak akan menyelesaikan ini untuk panggilan tidak langsung ke tabel virtual dan sebagai gantinya akan memanggil fungsi secara langsung
A::function()
. Jika suatu fungsi adalah virtual untuk tipe yang Anda gunakan, maka ia akan memutuskan panggilan ke tabel virtual yang akan selalu menjadi kelas yang paling diturunkan. Jika ini bukan virtual untuk jenis itu maka itu hanya akan memanggilType::function()
dan meneruskan pointer objek ke sana, cor ke Type.Sebenarnya ketika saya mengatakan pointer ke tabel virtualnya, itu sebenarnya selalu merupakan offset 16 ke dalam tabel virtual.
virtual
tidak diperlukan lagi di kelas yang lebih diturunkan jika itu virtual di kelas yang lebih rendah karena menyebar. Tetapi dapat digunakan untuk menunjukkan bahwa fungsi tersebut memang merupakan fungsi virtual, tanpa harus memeriksa kelas-kelas yang diwarisi tipe definisi.override
adalah penjaga kompiler lain yang mengatakan bahwa fungsi ini mengesampingkan sesuatu dan jika tidak maka melemparkan kesalahan kompilator.= 0
berarti bahwa ini adalah fungsi abstrakfinal
mencegah fungsi virtual diimplementasikan lagi di kelas yang lebih diturunkan dan akan memastikan bahwa tabel virtual dari kelas yang paling diturunkan berisi fungsi akhir dari kelas itu.= default
membuatnya eksplisit dalam dokumentasi bahwa kompiler akan menggunakan implementasi default= delete
berikan kesalahan penyusun jika panggilan ke ini dilakukanWarisan Virtual
Mempertimbangkan
Tanpa mewarisi kelas bass Anda akan mendapatkan objek yang terlihat seperti ini:
Alih-alih ini:
Yaitu akan ada 2 objek dasar.
Dalam situasi warisan berlian virtual di atas, setelah baru dipanggil, ia memanggil konstruktor yang paling diturunkan dan dalam konstruktor itu, ia memanggil semua 3 konstruktor turunan yang melewati offset ke dalam tabel tabel virtualnya, alih-alih memanggil hanya memanggil
DerivedClass1::DerivedClass1()
danDerivedClass2::DerivedClass2()
kemudian mereka berdua callingBase::Base()
Berikut ini semua dikompilasi dalam mode debug -O0 sehingga akan ada perakitan yang berlebihan
Itu panggilan
Base::Base()
dengan pointer ke objek offset 32. Base menyimpan pointer ke tabel virtualnya di alamat yang diterimanya dan anggota-anggotanya setelah itu.DerivedDerivedClass::DerivedDerivedClass()
kemudian memanggilDerivedClass1::DerivedClass1()
dengan pointer ke objek offset 0 dan juga melewati alamatVTT for DerivedDerivedClass+8
DerivedDerivedClass::DerivedDerivedClass()
kemudian melewati alamat dari objek + 16 dan alamat VTT untukDerivedDerivedClass+24
untukDerivedClass2::DerivedClass2()
yang perakitan identik denganDerivedClass1::DerivedClass1()
kecuali untuk garismov DWORD PTR [rax+8], 3
yang jelas memiliki 4 bukannya 3 untukd = 4
.Setelah ini, ia mengganti semua 3 pointer tabel virtual dalam objek dengan pointer ke offset di
DerivedDerivedClass
vtable ke representasi untuk kelas itu.d->VirtualFunction();
:d->DerivedCommonFunction();
:d->DerivedCommonFunction2();
:d->DerivedDerivedCommonFunction();
:((DerivedClass2*)d)->DerivedCommonFunction2();
:((Base*)d)->VirtualFunction();
:sumber