Как виртуальное наследование решает «алмаз» (многократное наследование) двусмысленность?

StackOverflow https://stackoverflow.com/questions/2659116

Вопрос

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, У него есть два пути, которые он может следовать, но не может решить сам по себе.

Итак, как виртуальное наследование разрешает проблему (помогите компилятору принять решение)?

Это было полезно?

Решение

Вы хотите: (Достижимо с виртуальным наследством)

  A  
 / \  
B   C  
 \ /  
  D 

И нет: (Что происходит без виртуального наследования)

A   A  
|   |
B   C  
 \ /  
  D 

Виртуальное наследование означает, что будет всего 1 экземпляр базы A Класс не 2.

Твой тип D будет иметь 2 указателя VTable (вы можете увидеть их на первой диаграмме), один для B и один за C Кто практически наследует A. DРазмер объекта увеличивается, потому что он хранит 2 указателя сейчас; Однако есть только один A Теперь.

Так B::A и C::A одинаковы, и поэтому не могут быть неоднозначных звонков от D. Отказ Если вы не используете виртуальное наследование, у вас есть вторая диаграмма выше. И любой звонок для члена A затем становится неоднозначным, и вам нужно указать, какой путь вы хотите взять.

У Википедии есть еще одна хорошая подводка и пример здесь

Другие советы

Исполнения полученных классов «содержат» экземпляры базовых классов, поэтому они выглядят в памяти, как это:

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

Итак, обратите внимание на две «копии» данных. Виртуальное наследование означает, что внутренний полученный класс существует указатель 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

Почему еще один ответ?

Ну, многие посты на так и статьи снаружи говорят, что алмазная проблема решается путем создания одного экземпляра 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. Определение параметров базового класса в классе внуков, а не прямого ребенка, не является хорошей практикой, поэтому оно должно быть допущено, когда существует алмазная проблема, и эта мера неизбежна.

Проблема не дорожка Компилятор должен следовать. Проблема в конечная точка Из этого пути: результат активов. Когда дело доходит до преобразований типа, путь не имеет значения, только окончательный результат.

Если вы используете обычное наследование, каждый путь имеет свою отличительную конечную точку, что означает, что результат активов неоднозначен, что является проблемой.

Если вы используете виртуальное наследование, вы получаете алмазную иерархию: оба пути приводят к той же конечной точке. В этом случае проблема выбора пути больше не существует (или, точнее, больше не имеет значения), потому что оба пути приводят к одному и тому же результату. Результат больше не является неоднозначным - то есть то, что имеет значение. Точный путь не.

На самом деле пример должен быть следующим:

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

... Таким образом, вывод будет правильным: «Ешь => D»

Виртуальное наследование решает только дублирование деда! Но вам все еще нужно указать методы, чтобы быть виртуальным, чтобы получить методы правильно переопределены ...

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top