Como herança virtual resolver o "diamante" (herança múltipla) ambiguidade?
-
27-09-2019 - |
Pergunta
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 diamante problema, e acima pedaço de código que não tem esse problema.
Não exatamente como herança virtual resolver o problema?
O que eu entendo:
Quando eu 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 ele tem dois caminhos que ele pode seguir, mas não pode decidir por si só.
Então, como herança virtual resolver o problema (o compilador de ajuda tomar a decisão)?
Solução
Você quer: (Alcançável com herança virtual)
A
/ \
B C
\ /
D
E não: (O que acontece sem herança virtual)
A A
| |
B C
\ /
D
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.
Outras dicas
Instâncias de classes derivadas "contêm" instâncias de classes base, para que eles olhem na memória assim:
class A: [A fields]
class B: [A fields | B fields]
class C: [A fields | C fields]
Assim, sem herança virtual, a instância da classe D seria:
class D: [A fields | B fields | A fields | C fields | D fields]
'- derived from B -' '- derived from C -'
Portanto, observe duas "cópias" de dados. A herança virtual significa que, na classe Derivada, há um ponteiro vtable definido em tempo de execução que aponta para dados da classe base, para que as instâncias das classes B, C e D se parecem:
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
Por uma outra resposta?
Bem, muitos posts sobre ISSO e artigos fora dizer, que o diamante problema é resolvido através da criação de instância única de A
em vez de dois (um para cada um dos pais de D
), assim resolver a ambiguidade.No entanto, isso não me dê compreensão abrangente do processo, acabei com ainda mais perguntas como
- o que se
B
eC
tenta criar diferentes instâncias deA
exemplo:chamando parametrizando o construtor com parâmetros diferentes (D::D(int x, int y): C(x), B(y) {}
)?A instância doA
será escolhido para se tornar parte deD
? - e se eu usar o não-virtual herança para
B
, mas virtual, paraC
?É o suficiente para criar uma única instância deA
noD
? - devo sempre usar o virtual herança por padrão a partir de agora, como medida preventiva, pois resolve possível diamante problema com o menor custo de desempenho e sem outros inconvenientes?
Não sendo capaz de predizer o comportamento sem tentar exemplos de código significa não compreender o conceito.Abaixo está o que me ajudou a envolver a cabeça em torno de herança virtual.
Dupla
Primeiro, vamos começar com este código, sem herança virtual:
#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;
}
Vamos passar por saída.Execução B b(2);
cria A(2)
como esperado, mesmo para 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);
precisa tanto de B
e C
, cada um deles a criação de seus próprios A
, por isso temos de casal A
no d
:
Create d(2,3):
A::A(2) C::C(2) A::A(3) B::B(3) D::D(2, 3)
Essa é a razão para d.getX()
a causa erro de compilação como compilador não pode escolher o que A
instância, ele deve chamar o método.Ainda é possível chamar métodos diretamente para o escolhido classe principal:
d.B::getX() = 3
d.C::getX() = 2
Virtualidade
Agora vamos adicionar herança virtual.Usando o mesmo exemplo de código com as seguintes alterações:
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
...
Permite saltar a criação da d
:
Create d(2,3):
A::A() C::C(2) B::B(3) D::D(2, 3)
Você pode ver, A
é criado com o construtor padrão ignorando parâmetros passados a partir de construtores de B
e C
.Como a ambigüidade é ido, todas as chamadas para getX()
retornar o mesmo valor:
d.getX() = 42
d.A::getX() = 42
d.B::getX() = 42
d.C::getX() = 42
Mas o que se deseja chamar parametrizando construtor para A
?Isso pode ser feito através de uma chamada explícita a partir do construtor de D
:
D(int x, int y, int z): A(x), C(y), B(z)
Normalmente, a classe pode usar explicitamente construtores direta dos pais, mas há uma exclusão de herança virtual caso.Descobrir a regra "clicado" para mim e ajudou-nos a compreender as interfaces virtuais muito:
Código class B: virtual A
significa que qualquer classe herdada de B
é agora responsável pela criação de A
por si só, uma vez que B
não vai fazer isso automaticamente.
Com esta afirmação em mente, é fácil responder a todas as perguntas que eu tinha:
- Durante
D
criação nemB
nemC
é responsável por parâmetros deA
, é totalmente atéD
só. C
irá delegar a criação deA
paraD
, masB
vai criar sua própria instância doA
trazendo diamante problema de volta- A definição da base de dados de classe de parâmetros neto de classe, ao invés de incluir direto criança não é uma boa prática, por isso deve ser tolerado quando diamante problema existe e esta medida é inevitável.
O problema não é o caminho o compilador deve seguir. O problema é o endpoint Desse caminho: o resultado do elenco. Quando se trata de digitar conversões, o caminho não importa, apenas o resultado final o faz.
Se você usa a herança comum, cada caminho tem seu próprio endpoint distinto, o que significa que o resultado do elenco é ambíguo, que é o problema.
Se você usa a herança virtual, obtém uma hierarquia em forma de diamante: ambos os caminhos levam ao mesmo ponto de extremidade. Nesse caso, o problema de escolher o caminho não existe mais (ou, mais precisamente, não importa mais), porque ambos os caminhos levam ao mesmo resultado. O resultado não é mais ambíguo - é isso que importa. O caminho exato não.
Na verdade, o exemplo deve ser o seguinte:
#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;
}
... Dessa forma, a saída será a correta: "Coma => d"
A herança virtual resolve apenas a duplicação do avô! Mas você ainda precisa especificar os métodos para serem virtuais para obter os métodos corretamente substituídos ...