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();
}
Eu entendo o problema do diamante, e o código acima não tem esse problema.
Como exatamente a herança virtual resolve o problema?
O que eu entendo:
Quando digo A *a = new D();
, o compilador quer saber se um objeto do tipo D
pode ser atribuído a um ponteiro do tipo A
, mas tem dois caminhos que pode seguir, mas não pode decidir por si mesmo.
Então, como a herança virtual resolve o problema (ajude o compilador a tomar a decisão)?
B
aC
implementação de ou em vez disso? Obrigado!Instâncias de classes derivadas "contêm" instâncias de classes básicas, de modo que se parecem com isso na memória:
Assim, sem herança virtual, a instância da classe D seria semelhante a:
Portanto, observe duas "cópias" dos dados A. Herança virtual significa que dentro da classe derivada há um ponteiro vtable definido em tempo de execução que aponta para os dados da classe base, de modo que as instâncias das classes B, C e D se parecem com:
fonte
Por que outra resposta?
Bem, muitos posts no SO e artigos externos dizem que o problema do diamante é resolvido criando uma instância única de em
A
vez de duas (uma para cada pai deD
), resolvendo assim a ambigüidade. No entanto, isso não me deu uma compreensão abrangente do processo, acabei com ainda mais perguntas comoB
e seC
tentar criar instâncias diferentes de,A
por exemplo, chamar o construtor parametrizado com parâmetros diferentes (D::D(int x, int y): C(x), B(y) {}
)? De qual instância deA
será escolhida para fazer parteD
?B
, mas virtualC
? É o suficiente para criar uma única instância deA
inD
?Não ser capaz de prever o comportamento sem tentar amostras de código significa não entender o conceito. Abaixo está o que me ajudou a entender a herança virtual.
Double A
Primeiro, vamos começar com este código sem herança virtual:
Vamos ver a saída. Executar
B b(2);
criaA(2)
conforme esperado, o mesmo paraC c(3);
:D d(2, 3);
precisa de ambosB
eC
, cada um deles criando o seu próprioA
, então temos o dobroA
emd
:Essa é a razão para
d.getX()
causar um erro de compilação, já que o compilador não pode escolher qualA
instância ele deve chamar o método. Ainda assim, é possível chamar métodos diretamente para a classe pai escolhida:Virtualidade
Agora vamos adicionar herança virtual. Usando o mesmo exemplo de código com as seguintes alterações:
Vamos pular para a criação de
d
:Você pode ver,
A
é criado com o construtor padrão, ignorando os parâmetros passados dos construtores deB
eC
. Como a ambiguidade se foi, todas as chamadas paragetX()
retornar o mesmo valor:Mas e se quisermos chamar o construtor parametrizado para
A
? Isso pode ser feito chamando-o explicitamente do construtor deD
:Normalmente, a classe pode usar explicitamente apenas construtores de pais diretos, mas há uma exclusão para o caso de herança virtual. Descobrir essa regra me "clicou" e ajudou muito a entender as interfaces virtuais:
Código
class B: virtual A
significa que qualquer classe herdada deB
agora é responsável por criarA
por si mesma, já queB
não vai fazer isso automaticamente.Com esta declaração em mente, é fácil responder a todas as minhas perguntas:
D
criação nemB
nemC
é responsável pelos parâmetros deA
, é totalmente dependente deD
apenas.C
vai delegar a criação deA
paraD
, masB
vai criar sua própria instância doA
trazendo problema diamante de voltafonte
O problema não é o caminho que o compilador deve seguir. O problema é o ponto final desse caminho: o resultado do elenco. Quando se trata de conversões de tipo, o caminho não importa, apenas o resultado final.
Se você usar herança comum, cada caminho terá seu próprio ponto final distinto, o que significa que o resultado da conversão é ambíguo, que é o problema.
Se você usar herança virtual, obterá uma hierarquia em forma de diamante: ambos os caminhos levam ao mesmo ponto final. Nesse caso, o problema de escolher o caminho não existe mais (ou, mais precisamente, não importa mais), pois os dois caminhos levam ao mesmo resultado. O resultado não é mais ambíguo - isso é o que importa. O caminho exato não.
fonte
Na verdade, o exemplo deve ser o seguinte:
... assim a saída será a correta: "EAT => D"
A herança virtual resolve apenas a duplicação do avô! MAS você ainda precisa especificar os métodos a serem virtuais para que os métodos sejam substituídos corretamente ...
fonte