Як віртуальне успадкування вирішує неоднозначність “діаманта” (багаторазового успадкування)?


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

Я розумію проблему з алмазами, і вище фрагмент коду не має цієї проблеми.

Як саме віртуальне успадкування вирішує проблему?

Що я розумію: Коли я кажу A *a = new D();, компілятор хоче знати, чи можна об’єкт типу Dприсвоїти покажчику типу A, але він має два шляхи, якими він може йти, але не може сам вирішити.

Отже, як віртуальна спадщина вирішує проблему (допоможе компілятор прийняти рішення)?

Відповіді:


109

Ви хочете: (Досяжний за допомогою віртуального успадкування)

  A  
 / \  
B   C  
 \ /  
  D 

А ні: (Що відбувається без віртуального успадкування)

A   A  
|   |
B   C  
 \ /  
  D 

Віртуальне успадкування означає, що базового Aкласу буде лише 1 екземпляр, а не 2.

Ваш тип Dмав би два вказівні покажчики (їх можна побачити на першій схемі), один для Bі той, Cхто фактично успадковує A. DРозмір об’єкта збільшено, оскільки в ньому зберігаються 2 покажчики; однак є лише один A.

Так B::Aі C::Aвони однакові, і тому не може бути ніяких неоднозначних дзвінків D. Якщо ви не використовуєте віртуальне успадкування, ви маєте другу схему вище. І будь-який дзвінок до члена A потім стає неоднозначним, і вам потрібно вказати, який шлях ви хочете пройти.

У Вікіпедії тут є ще один хороший пробіг та приклад


2
Покажчик Vtable - це деталь реалізації. Не всі компілятори будуть вводити vtable покажчики в цьому випадку.
curiousguy

19
Думаю, було б краще виглядати, якби графіки відображались вертикально. У більшості випадків я знаходив такі діаграми успадкування, щоб показати похідні класи під основами. (див. "занижений", "зсунутий")
петер - відновити Моніку

Як я можу змінити його код, щоб замість цього використати реалізацію Bабо Cs? Дякую!
Minh Nghĩa

44

Екземпляри похідних класів "містять" екземпляри базових класів, тому вони виглядають у пам'яті так:

class A: [A fields]
class B: [A fields | B fields]
class C: [A fields | C fields]

Таким чином, без віртуального успадкування екземпляр класу D буде виглядати так:

class D: [A fields | B fields | A fields | C fields | D fields]
          '- derived from B -' '- derived from C -'

Отже, зверніть увагу на дві "копії" даних A. Віртуальне успадкування означає, що всередині похідного класу є покажчик vtable, встановлений під час виконання, який вказує на дані базового класу, так що екземпляри класів B, C та D виглядають так:

class B: [A fields | B fields]
          ^---------- pointer to A

class C: [A fields | C fields]
          ^---------- pointer to A

class D: [A fields | B fields | C fields | D fields]
          ^---------- pointer to B::A
          ^--------------------- pointer to C::A


43

Чому інша відповідь?

Що ж, багато публікацій про SO та статті поза межами кажуть, що алмазна проблема вирішується шляхом створення одного примірника Aзамість двох (по одному для кожного з батьків D), тим самим вирішуючи двозначність. Однак це не дало мені всебічного розуміння процесу, у мене з’явилося ще більше запитань

  1. що робити, якщо Bі Cнамагається створити різні екземпляри, Aнаприклад, виклик параметризованого конструктора з різними параметрами ( D::D(int x, int y): C(x), B(y) {})? Який екземпляр Aбуде обраний, щоб стати частиною D?
  2. що робити, якщо я використовую невіртуальну спадщину для B, а віртуальну для C? Чи достатньо для створення одного примірника Aв D?
  3. Чи слід завжди використовувати віртуальну спадщину за замовчуванням як запобіжний захід, оскільки вона вирішує можливу алмазну проблему з незначною вартістю продуктивності та інших недоліків?

Неможливість передбачити поведінку без спроб зразків коду означає не розуміння концепції. Нижче - те, що допомогло мені обернути голову навколо віртуальної спадщини.

Подвійний А

По-перше, давайте почнемо з цього коду без віртуального успадкування:

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

Пройдемо через вихід. Виконання B b(2);створює, A(2)як очікувалося, те саме для 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);потреби як Bі Cкожен з них створює свій власний A, так що ми двічі Aв d:

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

Ось причина d.getX()викликати помилку компіляції, оскільки компілятор не може вибрати, для якого Aекземпляра він повинен викликати метод. Проте можливо викликати методи безпосередньо для вибраного батьківського класу:

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

Віртуальність

Тепер давайте додавати віртуальну спадщину. Використовуючи той самий зразок коду із наступними змінами:

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

Давайте перейдемо до створення d:

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

Ви можете бачити, що Aстворений за замовчуванням конструктор ігнорує параметри, передані від конструкторів Bі C. Оскільки двозначності немає, усі виклики getX()повертати одне і те ж значення:

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

Але що, якщо ми хочемо викликати параметризований конструктор для A? Це можна зробити, явно викликавши його з конструктора D:

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

Зазвичай клас може явно використовувати конструктори лише безпосередніх батьків, але існує виняток для випадку віртуального успадкування. Відкриття цього правила "натиснуло" для мене і допомогло зрозуміти віртуальні інтерфейси:

Код class B: virtual Aозначає, що будь-який клас, успадкований від B, тепер відповідає за створення Aсам, оскільки Bне збирається робити це автоматично.

Маючи на увазі це твердження, легко відповісти на всі мої запитання:

  1. Під час Dстворення ніхто Bне Cвідповідає за параметри A, і це повністю залежить від Dлише.
  2. Cделегуватиме створення Aдо D, але Bстворить власний екземплярA , таким чином , приносячи проблеми алмазів назад
  3. Визначення параметрів базового класу в класі онука, а не прямої дитини не є хорошою практикою, тому його слід допускати, коли існує проблема з алмазами, і цей захід неминучий.

10

Проблема не в шляху, яким повинен слідувати компілятор. Проблема - кінцева точка цього шляху: результат відбору. Що стосується перетворення типів, шлях не має значення, а лише кінцевий результат.

Якщо ви використовуєте звичайне успадкування, кожен шлях має свою відмітну кінцеву точку, тобто результат голосування неоднозначний, у чому полягає проблема.

Якщо ви використовуєте віртуальне успадкування, ви отримуєте алмазну ієрархію: обидва контури ведуть до однієї кінцевої точки. У цьому випадку проблема вибору шляху вже не існує (або, точніше, більше не має значення), оскільки обидва шляху ведуть до однакового результату. Результат вже не однозначний - ось що важливо. Точного шляху немає.


@Andrey: Як компілятор реалізує спадщину ... Я маю на увазі, що я отримую ваш аргумент, і я хочу подякувати вам за це так чітко пояснити ... але це дійсно допомогло б, якщо ви можете пояснити (або вказати на посилання) щодо як компілятор насправді реалізує успадкування і що змінюється, коли я роблю віртуальне успадкування
Брюс

8

Насправді приклад має бути таким:

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

... таким чином вихід буде правильним: "EAT => D"

Віртуальна спадщина вирішує лише дублювання діда! Але вам все одно потрібно вказати методи, які мають бути віртуальними, щоб правильно переоцінити методи ...

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.