Подтвердить что ты не робот

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

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

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

4b9b3361

Ответ 1

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

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

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

Ответ 2

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

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

Ответ 3

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

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

Ответ 4

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

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

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

Ответ 5

Собственно, пример должен быть следующим:

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

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

Ответ 6

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

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

Ответ 7

Что делать, если кто-то не может изменить код своего класса, я имею в виду, что у меня скомпилированные библиотеки содержат код того же типа, что я хочу использовать указатель базового класса A для работы с объектами других классов, как это позволяет виртуальным базовым программам, но я хочу этого без виртуального базового класса

Ответ 8

Эта проблема может быть решена с помощью ключевого слова 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;
}

Ответ 9

#include <iostream>

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

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