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 de D
), resolvendo assim a ambigüidade. No entanto, isso não me deu uma compreensão abrangente do processo, acabei com ainda mais perguntas como
B
e se C
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 de A
será escolhida para fazer parte D
?
- e se eu usar herança não virtual para
B
, mas virtual C
? É o suficiente para criar uma única instância de A
in D
?
- Devo sempre usar a herança virtual por padrão a partir de agora como medida preventiva, uma vez que resolve o possível problema do diamante com baixo custo de desempenho e sem outras desvantagens?
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:
#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 ver a saída. Executar B b(2);
cria A(2)
conforme esperado, o 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 de ambos B
e C
, cada um deles criando o seu próprio A
, então temos o dobro A
emd
:
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()
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:
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
...
Vamos pular para a criação de 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 os parâmetros passados dos construtores de B
e C
. Como a ambiguidade se foi, 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 e se quisermos chamar o construtor parametrizado para A
? Isso pode ser feito chamando-o explicitamente do construtor de D
:
D(int x, int y, int z): A(x), C(y), B(z)
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 de B
agora é responsável por criar A
por si mesma, já que B
não vai fazer isso automaticamente.
Com esta declaração em mente, é fácil responder a todas as minhas perguntas:
- Durante a
D
criação nem B
nem C
é responsável pelos parâmetros de A
, é totalmente dependente de D
apenas.
C
vai delegar a criação de A
para D
, masB
vai criar sua própria instância do A
trazendo problema diamante de volta
- Definir parâmetros de classe base na classe neto em vez de filho direto não é uma boa prática, portanto, deve ser tolerado quando existe o problema do diamante e esta medida é inevitável.