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


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 63

7 ответов:

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

  A  
 / \  
B   C  
 \ /  
  D 

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

A   A  
|   |
B   C  
 \ /  
  D 

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

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

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

Википедия имеет еще один хороший обзор и пример здесь

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

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"

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

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

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

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

Double 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 экземпляр он должен вызвать метод for. Тем не менее, можно вызвать методы непосредственно для выбранного родительского класса:

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