Sanal miras, "elmas" (çoklu miras) belirsizliğini nasıl çözer?


97
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(); 
} 

Elmas problemini anlıyorum ve yukarıdaki kod parçasında bu problem yok.

Sanal kalıtım sorunu tam olarak nasıl çözer?

Ne anlıyorum: Dediğimde A *a = new D();, derleyici bir türdeki nesnenin bir tür Dişaretçisine atanıp atanamayacağını bilmek istiyor A, ancak izleyebileceği, ancak kendi başına karar veremeyeceği iki yolu var.

Peki, sanal miras sorunu nasıl çözer (derleyicinin kararı almasına yardımcı olur)?

Yanıtlar:


112

İstediğiniz: (Sanal kalıtımla elde edilebilir)

  A  
 / \  
B   C  
 \ /  
  D 

Ve değil: (Sanal kalıtım olmadan ne olur)

A   A  
|   |
B   C  
 \ /  
  D 

Sanal kalıtım, A2 değil , temel sınıfın yalnızca 1 örneği olacağı anlamına gelir .

Sizin tipi D, (önce grafikte bunları görebilir) için bir tane 2 vtable işaretçileri olurdu Bve bir tane Csanal olarak miras kim A. DŞimdi 2 işaretçi depoladığı için nesnenin boyutu artırıldı; ancak Aşimdi sadece bir tane var.

Yani B::Ave C::Aaynı ve bu yüzden belirsiz aramalar olamaz D. Sanal kalıtım kullanmıyorsanız, yukarıdaki ikinci diyagrama sahipsiniz. Ve bir A üyesine yapılan herhangi bir çağrı belirsiz hale gelir ve hangi yolu gitmek istediğinizi belirlemeniz gerekir.

Wikipedia'da başka bir güzel özet ve örnek var


2
Vtable işaretçisi bir uygulama detayıdır. Bu durumda tüm derleyiciler vtable işaretçileri sunmayacaktır.
wonderguy

19
Grafikler dikey olarak yansıtılırsa daha iyi görüneceğini düşünüyorum. Çoğu durumda, türetilmiş sınıfları tabanların altında göstermek için bu tür miras diyagramları buldum. (bkz. "downcast", "upcast")
peterh - Monica'yı yeniden etkinleştir

Bunun yerine B's veya C' uygulamasını kullanmak için kodunu nasıl değiştirebilirim ? Teşekkürler!
Minh Nghĩa

46

Neden başka bir cevap?

Peki, SO'daki birçok gönderi ve dışarıdaki makaleler, elmas probleminin Aiki yerine tek bir örnek oluşturarak çözüldüğünü söyler (her ebeveyn için bir tane D), böylece belirsizliği çözer. Ancak, bu bana süreci kapsamlı bir şekilde anlamamı sağladı, daha da fazla soru ile karşılaştım.

  1. ne olur Bve örneğin farklı parametrelerle ( ) parametrize kurucu çağırmanın Cfarklı örneklerini yaratmaya çalışırsa ? Hangi örneği parçası olmak için seçilecek ?AD::D(int x, int y): C(x), B(y) {}AD
  2. için sanal olmayan kalıtım kullanıyorsam B, ancak sanal olanı kullanıyorsam C? Bunun tek bir örneğini oluşturmak için yeterli mi Aiçinde D?
  3. Muhtemel elmas problemini düşük performans maliyeti ve başka sakıncaları olmadan çözdüğü için bundan böyle önleyici tedbir olarak her zaman varsayılan olarak sanal kalıtımı kullanmalı mıyım?

Kod örneklerini denemeden davranışı tahmin edememek, kavramı anlamamak demektir. Aşağıda, sanal mirasın etrafını sarmama yardımcı olan şey var.

Çift a

İlk olarak, sanal miras olmadan bu kodla başlayalım:

#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;
}

Çıktıya geçelim. Yürütme B b(2);, A(2)beklendiği gibi oluşturur , aynı şekilde 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);hem ihtiyacı Bve Cbunların her biri kendi yaratarak, Abiz çift var bu yüzden, Aiçinde d:

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

d.getX()Derleyici hangi Aörnek için yöntemi çağıracağını seçemediği için derleme hatasına neden olmasının nedeni budur . Yine de seçilen üst sınıf için yöntemleri doğrudan çağırmak mümkündür:

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

Sanallık

Şimdi sanal miras ekleyelim. Aşağıdaki değişikliklerle aynı kod örneğini kullanma:

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
...

Şunların oluşturulmasına geçelim d:

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

Sen görebilirsiniz Avarsayılan yapıcı yapıcıları geçirilen parametreleri göz ardı oluşturulur Bve C. Belirsizlik ortadan kalktıkça, getX()aynı değeri döndürmek için tüm çağrılar :

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

Peki ya parametrize kurucu için çağırmak istersek A? Bunu yapıcıdan açıkça çağırarak yapılabilir D:

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

Normalde, sınıf yalnızca doğrudan ebeveynlerin oluşturucularını kullanabilir, ancak sanal miras durumu için bir dışlama vardır. Bu kuralı keşfetmek benim için "tıklandı" ve sanal arayüzlerin anlaşılmasına çok yardımcı oldu:

Kod class B: virtual A, miras alınan herhangi bir sınıfın Bartık Akendi başına oluşturmaktan sorumlu olduğu anlamına gelir, çünkü Bbunu otomatik olarak yapmayacaktır.

Bu ifade aklıma geldiğinde sahip olduğum tüm soruları yanıtlamak kolaydır:

  1. DYaratılış sırasında ne parametrelerinden ne Bde Csorumlu değildir , Atamamen Dsadece kalmıştır .
  2. Ciçin yaratma yetkisini Averecek D, ancak Bkendi örneğini yaratacak ve Aböylece elmas problemini geri getirecek
  3. Direkt çocuk yerine torun sınıfında temel sınıf parametrelerini tanımlamak iyi bir uygulama değildir, bu nedenle elmas problemi olduğunda ve bu önlem kaçınılmaz olduğunda buna tolerans gösterilmelidir.

Bu cevap son derece bilgilendirici! Özellikle virtualanahtar kelimeyi "daha sonra tanımlanmış (alt sınıflarda)" olarak yorumlamanız , yani "gerçekten" tanımlı değil, "sanal olarak" tanımlı. Bu yorum sadece temel sınıflar için değil aynı zamanda yöntemler için de işe yarar. Teşekkür ederim!
Maggyero

45

Türetilmiş sınıfların örnekleri, temel sınıflarının üyelerini depolar .

Sanal miras olmadan , bellek düzenleri şöyle görünür ( sınıftaki üyelerin iki kopyasına dikkat edin ):AD

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]

Sanal miras ile, bellek düzenleri şöyle görünür ( sınıftaki üyelerin tek kopyasına dikkat edin ):AD

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 ----------------|

Her türetilmiş sınıf için derleyici, türetilmiş sınıfta depolanan sanal temel sınıflarının üyelerine işaretçiler tutan sanal bir tablo oluşturur ve türetilmiş sınıftaki bu sanal tabloya bir işaretçi ekler.



10

Sorun, derleyicinin izlemesi gereken yol değil . Sorun, bu yolun son noktasıdır : dönüşümün sonucudur. Yazma dönüşümleri söz konusu olduğunda, yol önemli değildir, yalnızca nihai sonuç önemlidir.

Sıradan kalıtım kullanırsanız, her yolun kendine özgü bir son noktası vardır, bu da dökümün sonucunun belirsiz olduğu anlamına gelir, bu da sorun.

Sanal miras kullanırsanız, elmas şeklinde bir hiyerarşi elde edersiniz: her iki yol da aynı uç noktaya götürür. Bu durumda, yolu seçme sorunu artık mevcut değildir (veya daha doğrusu artık önemli değildir), çünkü her iki yol da aynı sonuca götürür. Sonuç artık belirsiz değil - önemli olan bu. Kesin yol değil.


@Andrey: Derleyici kalıtımı nasıl uyguluyor ... Yani argümanınızı anlıyorum ve bunu çok net bir şekilde açıkladığınız için size teşekkür etmek istiyorum ... ama açıklayabilirseniz (veya bir referansa işaret edebilirseniz) gerçekten yardımcı olur. derleyici kalıtımı gerçekte nasıl uyguluyor ve sanal kalıtım yaptığımda neler değişiyor
Bruce

8

Aslında örnek aşağıdaki gibi olmalıdır:

#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;
}

... bu şekilde çıktı doğru olacaktır: "EAT => D"

Sanal kalıtım yalnızca büyükbabanın kopyalanmasını çözer! AMA, yöntemleri doğru bir şekilde geçersiz kılmak için yine de sanal olacak yöntemleri belirtmeniz gerekiyor ...

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.