Por que há ambiguidade nesse padrão de diamante?
-
27-09-2019 - |
Pergunta
#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();
}
Não tenho certeza de que isso se chama Diamond Problem ou não, mas por que isso não funciona?
Eu dei a definição para eat()
por D
. Então, também não precisa usar B
's ou C
cópia (então, não deve haver problema).
Quando eu disse, a->eat()
(lembrar eat()
não é virtual), existe apenas um possível eat()
para ligar, o de A
.
Por que então eu recebo este erro:
'A' é uma base ambígua de 'd'
O que exatamente faz A *a = new D();
significa para o compilador ??
e
Por que o mesmo problema não ocorre quando eu uso D *d = new D();
?
Solução
Imagine um cenário um pouco diferente
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();
}
Se isso funcionasse, aumentaria o a
dentro B
ou o a
dentro C
? É por isso que é ambíguo. o this
ponteiro e qualquer membro de dados não estáticos é distinto para os dois A
subobjetos (um dos quais está contido pelo B
subobjeto e o outro pelo C
subobjeto). Tente alterar seu código como este e funcionará (na medida em que compila e imprime "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();
}
Isso vai ligar eat
no A
Subobjeto de B
e C
respectivamente.
Outras dicas
O diamante resulta em duas instâncias de A no objeto D, e é ambíguo a que você está se referindo - você precisa usar a herança virtual para resolver isso:
class B: virtual public A { public: void eat(){ cout<<"B";} };
class C: virtual public A { public: void eat(){ cout<<"C";} };
Supondo que você realmente quis apenas uma instância. Eu também suponho que você realmente quis dizer:
class D: public B, public C { public: void eat(){ cout<<"D";} };
Observe que o erro de compilação está no "a *a = new d ();" linha, não na chamada para "comer".
O problema é que, como você usou herança não virtual, acaba com a Classe A duas vezes: uma vez a B, e uma vez por C. Se, por exemplo, adicionar um membro M a A, então D tem dois deles: B :: m e c :: m.
Às vezes, você realmente deseja ter uma duas vezes no gráfico de derivação; nesse caso, sempre precisa indicar de qual está falando. Em D, você seria capaz de fazer referência a B :: M e C :: M separadamente.
Às vezes, porém, você realmente quer apenas um a, nesse caso, você precisa usar herança virtual.
Para uma situação verdadeiramente incomum, a resposta de Neil está realmente errada (pelo menos em parte).
Com Fora herança virtual, você obtém duas cópias separadas de A
no objeto final.
"The Diamond" resulta em uma única cópia de A
no objeto final e é produzido por Usando herança virtual:
Já que "The Diamond" significa que há apenas 1 cópia de A
no objeto final, uma referência a A
não produz ambiguidade. Sem herança virtual, uma referência a A
poderia se referir a um dos dois objetos diferentes (o da esquerda ou da direita no diagrama).
O erro que você está recebendo não está vindo de ligar eat()
- Está vindo da linha antes. É a própria Upcast que cria a ambiguidade. Como Neil Butterworth aponta, existem duas cópias de A
na tua D
, e o compilador não sabe qual você deseja a
apontar para.
Você quer: (Alcançável com herança virtual)
D
/ \
B c
\ /
UMA
E não: (O que acontece sem herança virtual)
D
/ \
B c
| |
A
Herança virtual significa que haverá apenas 1 instância da base A
classe não 2.
Seu tipo D
teria 2 ponteiros vtable (você pode vê -los no primeiro diagrama), um para B
e um para C
que praticamente herdam A
. D
O tamanho do objeto do objeto é aumentado porque armazena 2 ponteiros agora; No entanto, existe apenas um A
agora.
Então B::A
e C::A
são iguais e, portanto, não pode haver chamadas ambíguas de D
. Se você não usa a herança virtual, terá o segundo diagrama acima. E qualquer chamada para um membro de A então se torna ambígua e você precisa especificar qual caminho deseja seguir.