Как виртуальное наследование решает неоднозначность «алмазного» (множественного наследования)? - PullRequest
75 голосов
/ 17 апреля 2010
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 ]

86 голосов
/ 17 апреля 2010

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

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

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

39 голосов
/ 17 апреля 2010

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

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
15 голосов
/ 15 июля 2018

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

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

Неспособность предсказать поведение без использования примеров кода означает непонимание концепции. Вот что помогло мне обернуть голову вокруг виртуального наследования.

Двухместный A

Сначала давайте начнем с этого кода без виртуального наследования:

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

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

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

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

8 голосов
/ 15 января 2016

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

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

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

0 голосов
/ 14 марта 2018

Пример правильного кода здесь. Проблема с бриллиантами:

#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;
}
0 голосов
/ 15 ноября 2016

Эту проблему можно решить с помощью ключевого слова 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;
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...