Bagaimana warisan virtual menyelesaikan ambiguitas "berlian" (multiple inheritance)?


96
class A                     { public: void eat(){ cout<<"A";} }; 
class B: virtual public A   { public: void eat(){ cout<<"B";} }; 
class C: virtual public A   { public: void eat(){ cout<<"C";} }; 
class D: public         B,C { public: void eat(){ cout<<"D";} }; 

int main(){ 
    A *a = new D(); 
    a->eat(); 
} 

Saya memahami masalah berlian, dan potongan kode di atas tidak memiliki masalah itu.

Bagaimana sebenarnya warisan virtual menyelesaikan masalah?

Apa yang saya pahami: Ketika saya katakan A *a = new D();, kompilator ingin tahu apakah suatu objek bertipe Ddapat ditugaskan ke penunjuk tipe A, tetapi ia memiliki dua jalur yang dapat diikuti, tetapi tidak dapat memutuskan sendiri.

Jadi, bagaimana warisan virtual menyelesaikan masalah (membantu penyusun mengambil keputusan)?

Jawaban:


110

Anda menginginkan: (Dapat dicapai dengan warisan virtual)

  A  
 / \  
B   C  
 \ /  
  D 

Dan bukan: (Apa yang terjadi tanpa warisan virtual)

A   A  
|   |
B   C  
 \ /  
  D 

Warisan virtual berarti bahwa hanya akan ada 1 instance dari Akelas dasar, bukan 2.

Tipe Anda Dakan memiliki 2 pointer vtable (Anda dapat melihatnya di diagram pertama), satu untuk Bdan satu lagi untuk Cyang secara virtual mewarisi A. DUkuran objek bertambah karena sekarang menyimpan 2 pointer; Namun Asekarang hanya ada satu .

Begitu B::Adan C::Asama sehingga tidak ada panggilan yang ambigu dari D. Jika Anda tidak menggunakan warisan virtual, Anda memiliki diagram kedua di atas. Dan setiap panggilan ke anggota A kemudian menjadi ambigu dan Anda perlu menentukan jalur mana yang ingin Anda ambil.

Wikipedia memiliki ikhtisar dan contoh bagus lainnya di sini


2
Pointer Vtable adalah detail implementasi. Tidak semua kompiler akan memperkenalkan pointer vtable dalam kasus ini.
penasaran

19
Saya pikir akan terlihat lebih baik jika grafiknya dicerminkan secara vertikal. Dalam kebanyakan kasus, saya telah menemukan diagram pewarisan seperti itu untuk menunjukkan kelas turunan di bawah basis. (lihat "downcast", "upcast")
peterh - Reinstate Monica

Bagaimana saya bisa mengubah kodenya untuk menggunakan B's or C' s implementasi sebagai gantinya? Terima kasih!
Minh Nghĩa

45

Instance dari kelas turunan menyimpan anggota dari kelas dasarnya.

Tanpa warisan virtual, tata letak memori akan terlihat seperti (perhatikan dua salinan Aanggota di kelas D):

class A: [A members]
class B: public A [A members|B members]
class C: public A [A members|C members]
class D: public B, public C [A members|B members|A members|C members|D members]

Dengan warisan virtual, tata letak memori terlihat seperti (perhatikan salinan tunggalA anggota di kelas D):

class A: [A members]
class B: virtual public A [B members|A members]
                           |         ^
                           v         |
                         virtual table B

class C: virtual public A [C members|A members]
                           |         ^
                           v         |
                         virtual table C

class D: public B, public C [B members|C members|D members|A members]
                             |         |                   ^
                             v         v                   |
                           virtual table D ----------------|

Untuk setiap kelas turunan, compiler membuat tabel virtual yang menampung pointer ke anggota kelas basis virtualnya yang disimpan di kelas turunan, dan menambahkan pointer ke tabel virtual tersebut di kelas turunan.



44

Mengapa jawaban lain?

Nah, banyak posting di SO dan artikel di luar mengatakan, bahwa masalah berlian diselesaikan dengan membuat satu instance, Abukan dua (satu untuk setiap induk D), sehingga menyelesaikan ambiguitas. Namun, ini tidak memberi saya pemahaman yang komprehensif tentang proses, saya berakhir dengan lebih banyak pertanyaan seperti

  1. bagaimana jika Bdan Cmencoba untuk membuat contoh yang berbeda Amisalnya memanggil konstruktor parametrized dengan parameter berbeda ( D::D(int x, int y): C(x), B(y) {})? Contoh mana yang Aakan dipilih untuk menjadi bagian D?
  2. bagaimana jika saya menggunakan warisan non-virtual B, tetapi untuk warisan virtual C? Apakah cukup untuk membuat satu contoh AdalamD ?
  3. haruskah saya selalu menggunakan warisan virtual secara default mulai sekarang sebagai tindakan pencegahan karena ini memecahkan masalah berlian yang mungkin terjadi dengan biaya kinerja yang kecil dan tidak ada kekurangan lainnya?

Tidak dapat memprediksi perilaku tanpa mencoba contoh kode berarti tidak memahami konsepnya. Di bawah ini adalah apa yang membantu saya memahami warisan virtual.

Ganda A

Pertama, mari kita mulai dengan kode ini tanpa warisan virtual:

#include<iostream>
using namespace std;
class A {
public:
    A()                { cout << "A::A() "; }
    A(int x) : m_x(x)  { cout << "A::A(" << x << ") "; }
    int getX() const   { return m_x; }
private:
    int m_x = 42;
};

class B : public A {
public:
    B(int x):A(x)   { cout << "B::B(" << x << ") "; }
};

class C : public A {
public:
    C(int x):A(x) { cout << "C::C(" << x << ") "; }
};

class D : public C, public B  {
public:
    D(int x, int y): C(x), B(y)   {
        cout << "D::D(" << x << ", " << y << ") "; }
};

int main()  {
    cout << "Create b(2): " << endl;
    B b(2); cout << endl << endl;

    cout << "Create c(3): " << endl;
    C c(3); cout << endl << endl;

    cout << "Create d(2,3): " << endl;
    D d(2, 3); cout << endl << endl;

    // error: request for member 'getX' is ambiguous
    //cout << "d.getX() = " << d.getX() << endl;

    // error: 'A' is an ambiguous base of 'D'
    //cout << "d.A::getX() = " << d.A::getX() << endl;

    cout << "d.B::getX() = " << d.B::getX() << endl;
    cout << "d.C::getX() = " << d.C::getX() << endl;
}

Mari kita lihat keluaran. Menjalankan B b(2);pembuatan A(2)seperti yang diharapkan, sama untuk C c(3);:

Create b(2): 
A::A(2) B::B(2) 

Create c(3): 
A::A(3) C::C(3) 

D d(2, 3);kebutuhan baik Bdan C, masing-masing dari mereka menciptakan sendiri A, jadi kita harus ganda Adi d:

Create d(2,3): 
A::A(2) C::C(2) A::A(3) B::B(3) D::D(2, 3) 

Itulah alasan untuk d.getX()menyebabkan kesalahan kompilasi karena kompiler tidak dapat memilih Ainstance mana yang harus dipanggil metode. Masih mungkin untuk memanggil metode secara langsung untuk kelas induk yang dipilih:

d.B::getX() = 3
d.C::getX() = 2

Virtualitas

Sekarang mari tambahkan warisan virtual. Menggunakan sampel kode yang sama dengan perubahan berikut:

class B : virtual public A
...
class C : virtual public A
...
cout << "d.getX() = " << d.getX() << endl; //uncommented
cout << "d.A::getX() = " << d.A::getX() << endl; //uncommented
...

Mari melompat ke pembuatan d:

Create d(2,3): 
A::A() C::C(2) B::B(3) D::D(2, 3) 

Anda dapat melihat, Adibuat dengan konstruktor default mengabaikan parameter yang diteruskan dari konstruktor Bdan C. Saat ambiguitas hilang, semua panggilan untuk getX()mengembalikan nilai yang sama:

d.getX() = 42
d.A::getX() = 42
d.B::getX() = 42
d.C::getX() = 42

Tetapi bagaimana jika kita ingin memanggil konstruktor parametrized A? Ini dapat dilakukan dengan memanggilnya secara eksplisit dari konstruktor D:

D(int x, int y, int z): A(x), C(y), B(z)

Biasanya, kelas dapat secara eksplisit menggunakan konstruktor hanya dari orang tua langsung, tetapi ada pengecualian untuk kasus warisan virtual. Menemukan aturan ini "diklik" untuk saya dan sangat membantu memahami antarmuka virtual:

Kode class B: virtual Aberarti, setiap kelas yang diwarisi dari Bsekarang bertanggung jawab untuk membuat Asendiri, karena Btidak akan melakukannya secara otomatis.

Dengan mengingat pernyataan ini, mudah untuk menjawab semua pertanyaan yang saya miliki:

  1. Selama Dpembuatan tidak Bjuga Cbertanggung jawab atas parameter A, itu sepenuhnya terserah Dsaja.
  2. Cakan mendelegasikan pembuatan Ake D, tetapi Bakan membuat instance sendiri Asehingga mengembalikan masalah berlian
  3. Mendefinisikan parameter kelas dasar di kelas cucu daripada anak langsung bukanlah praktik yang baik, jadi harus ditoleransi ketika masalah intan ada dan ukuran ini tidak dapat dihindari.

Jawaban ini sangat informatif! Terutama interpretasi Anda atas virtualkata kunci sebagai "ditentukan nanti (dalam subclass)", artinya tidak "benar-benar" ditentukan tetapi "secara virtual". Interpretasi ini tidak hanya berfungsi untuk kelas dasar tetapi juga untuk metode. Terima kasih!
Maggyero

10

Masalahnya bukanlah path yang harus diikuti oleh compiler. Masalahnya adalah titik akhirnya dari jalan itu: hasil dari para pemain. Dalam hal konversi jenis, jalur tidak menjadi masalah, hanya hasil akhirnya yang menentukan.

Jika Anda menggunakan pewarisan biasa, setiap jalur memiliki titik akhir tersendiri, yang berarti bahwa hasil pemerannya ambigu, itulah masalahnya.

Jika Anda menggunakan warisan virtual, Anda mendapatkan hierarki berbentuk berlian: kedua jalur mengarah ke titik akhir yang sama. Dalam hal ini masalah pemilihan jalan sudah tidak ada lagi (atau, lebih tepatnya, tidak penting lagi), karena kedua jalan tersebut mengarah pada hasil yang sama. Hasilnya tidak lagi ambigu - itulah yang penting. Jalan yang tepat tidak.


@ Andrey: Bagaimana cara kompilator mengimplementasikan warisan ... Maksud saya, saya mendapatkan argumen Anda dan saya ingin berterima kasih karena telah menjelaskannya dengan sangat gamblang..tetapi akan sangat membantu jika Anda dapat menjelaskan (atau menunjuk ke referensi) tentang bagaimana sebenarnya kompilator mengimplementasikan pewarisan dan apa yang berubah ketika saya melakukan pewarisan virtual
Bruce

8

Sebenarnya contohnya harus sebagai berikut:

#include <iostream>

//THE DIAMOND PROBLEM SOLVED!!!
class A                     { public: virtual ~A(){ } virtual void eat(){ std::cout<<"EAT=>A";} }; 
class B: virtual public A   { public: virtual ~B(){ } virtual void eat(){ std::cout<<"EAT=>B";} }; 
class C: virtual public A   { public: virtual ~C(){ } virtual void eat(){ std::cout<<"EAT=>C";} }; 
class D: public         B,C { public: virtual ~D(){ } virtual void eat(){ std::cout<<"EAT=>D";} }; 

int main(int argc, char ** argv){
    A *a = new D(); 
    a->eat(); 
    delete a;
}

... dengan begitu hasilnya akan menjadi benar: "EAT => D"

Warisan virtual hanya menyelesaikan duplikasi kakek! TAPI Anda masih perlu menentukan metode menjadi virtual untuk mendapatkan metode diganti dengan benar ...

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.