Как виртуальное наследование решает неоднозначность "алмазного" (множественного наследования)?

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

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

7 ответов

Решение

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

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

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

Почему другой ответ?

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

Экземпляры производных классов "содержат" экземпляры базовых классов, поэтому они выглядят в памяти следующим образом:

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

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

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

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

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

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

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

Правильный пример кода здесь. Алмазная проблема:

#include <iostream>
// Here you have the diamond problem : there is B::eat() and C::eat()
// because they both inherit from A and contain independent copies of A::eat()
// So what is D::eat()? Is it B::eat() or C::eat() ?
class A { public: void eat(){ std::cout << "CHROME-CHROME" << endl; } };
class B: public A   { };
class C: public A   { };
class D: public B,C { };

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

Решение:

#include <iostream>
// Virtual inheritance to ensure B::eat() and C::eat() to be the same 
class A { public: void eat(){ std::cout<< "CHROME-CHROME" << endl; } };
class B: virtual public A   { };
class C: virtual public A   { };
class D: public         B,C { };

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

Эту проблему можно решить с помощью ключевого слова Virtual.

  A  
 / \ 
B   C  
 \ /  
  D 

Пример алмазной проблемы.

#include<stdio.h>
using namespace std;
class AA
{
    public:
            int a;
        AA()
            {
                a=10;   
            }
};
class BB: virtual public AA
{
    public:
            int b;
        BB()
            {
                b=20;   
            }
};
class CC:virtual public AA
{
    public:
            int c;
        CC()
            {
                c=30;   
            }
};
class DD:public BB,CC
{
    public:
            int d;
        DD()
            {
                d=40;
                printf("Value of A=%d\n",a);                
            }
};
int main()
{
    DD dobj;
    return 0;
}
Другие вопросы по тегам