#include <iostream>
using namespace std;
class A { public: void eat(){ cout<<"A";} };
class B: public A { public: void eat(){ cout<<"B";} };
class C: 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();
}
Я не уверен, что это называют ромбовидной проблемой или нет, но почему это не работает?
Я дал определение для eat()
для D
. Так, это не должно использовать также B
или C
копия (так, не должно быть никакой проблемы).
Когда я сказал, a->eat()
(помните eat()
не является виртуальным), существует только один возможный eat()
звонить, тот из A
.
Почему затем, сделайте я получаю эту ошибку:
Неоднозначной основы 'D'
Что точно делает A *a = new D();
значить для компилятора??
и
Почему та же проблема не происходит, когда я использую D *d = new D();
?
Представьте себе немного другой сценарий
class A { protected: int a; public: void eat(){ a++; cout<<a;} };
class B: public A { public: void eat(){ cout<<a;} };
class C: public A { public: void eat(){ cout<<a;} };
class D: public B,C { public: void eat(){ cout<<"D";} };
int main(){
A *a = new D();
a->eat();
}
Если это сработает, увеличится ли a
в B
или a
в C
? Вот почему это неоднозначно. Указатель this
и любой нестатический элемент данных различны для двух подобъектов A
(один из которых содержится в подобъекте B
, а другой - в подобъект C
). Попробуйте изменить свой код таким образом, и он будет работать (компилируется и печатает "A")
class A { public: void eat(){ cout<<"A";} };
class B: public A { public: void eat(){ cout<<"B";} };
class C: public A { public: void eat(){ cout<<"C";} };
class D: public B, public C { public: void eat(){ cout<<"D";} };
int main(){
A *a = static_cast<B*>(new D());
// A *a = static_cast<C*>(new D());
a->eat();
}
Это вызовет eat
в подобъекте A
B
и C
соответственно.
Ромб дает ДВА экземпляра A в объекте D, и неясно, какой из них вы имеете в виду - вам нужно использовать виртуальное наследование для решения this:
class B: virtual public A { public: void eat(){ cout<<"B";} };
class C: virtual public A { public: void eat(){ cout<<"C";} };
при условии, что вам действительно нужен только один экземпляр. Я также предполагаю, что вы действительно имели в виду:
class D: public B, public C { public: void eat(){ cout<<"D";} };
Обратите внимание, что ошибка компиляции связана с "A * a = new D ();" линия, а не на призыв "есть".
Проблема в том, что из-за того, что вы использовали не виртуальное наследование, вы получаете класс A дважды: один раз через B и один раз через C. Если, например, вы добавляете член m в A, то у D их два: B :: m и C :: m.
Иногда вам действительно нужно иметь A дважды в графе деривации, и в этом случае вам всегда нужно указывать, о каком A вы говорите. В D вы можете ссылаться на B :: m и C :: m по отдельности.
Однако иногда вам действительно нужен только один A, и в этом случае вам нужно использовать виртуальное наследование .
Ошибка возникает не из-за вызова eat ()
, а из предыдущей линии. Неоднозначность создает само апкастинг. Как указывает Нил Баттерворт, в вашем D
есть две копии A
, и компилятор не знает, на какую из них вы хотите указать a
.
Вы хотите: (достижимо с виртуальным наследованием)
D
/ \
B C
\ /
A
А не: (Что происходит без виртуального наследования)
D
/ \
B C
| |
A A
Виртуальное наследование означает, что будет только 1 экземпляр базового A
класса, а не 2.
Ваш тип D
будет иметь 2 указателя vtable (вы можете увидеть их на первой диаграмме), один для B
и один для C
, которые фактически наследуют A
. Размер объекта D
увеличен, потому что теперь он хранит 2 указателя; однако сейчас есть только один A
.
Итак, B :: A
и C :: A
одинаковы, и поэтому не может быть неоднозначных вызовов от D
. Если вы не используете виртуальное наследование, у вас есть вторая диаграмма выше. И тогда любой вызов члена A становится неоднозначным, и вам нужно указать, по какому пути вы хотите пойти.
В Википедии есть еще одно хорошее краткое изложение и пример здесь
Для действительно необычной ситуации ответ Нила на самом деле неверен (по крайней мере, частично).
При виртуальном наследовании out вы получаете две отдельные копии A
в конечном объекте.
«Алмаз» приводит к единственной копии A
в конечном объекте и создается с помощью с использованием виртуального наследования:
Поскольку «ромб» означает, что есть только одна копия A
в конечном объекте, ссылка на A
не вызывает двусмысленности. Без виртуального наследования ссылка на A
может относиться к любому из двух разных объектов (один слева или один справа на диаграмме).