OK, todos sabemos lo poderosa que es la herencia. Hasta aquí, nada nuevo. Sabemos también que C++ provée herencia múltiple. Algunos lenguajes autollamados "modernos" excluyen la herencia múltiple por considerar que su empleo nos puede llevar a muchos errores. Aunque esto sea cierto, el chiste está en capacitar mejor a los programadores en lugar de tratarlos como niños y quitarles expresividad a los programas.
En fin, qué se le va a hacer. Uno de los argumentos más empleados es el siguiente. Consideremos la clase A. Ahora supongamos que tenemos una clase B que deriva de A, y una clase C que deriva también de A. Luego, supongamos que tenemos una clase D que deriva tanto de A como de B. ¿Ven cuál es el llamado problema? ¿No es claro? Veámoslo en código.
class A
{
public:
virtual string Foo() { return "A::Foo"; }
};
class B : public A
{
public:
virtual string Foo() { return "B::Foo"; }
};
class C : public A
{
public:
virtual string Foo() { return "C::Foo"; }
};
class D : public B, public C
{
};
int main()
{
D d;
d.Foo();
return EXIT_SUCCESS;
}
En el ejemplo anterior, desde la perspectiva de D, existen dos versiones de
Foo, la de la clase B y la de la clase C. En la función
main, ¿a qué versión deberá llamar D, a la versión de B o la versión de C? Un problema comunmente llamado "del diamante".
Por supuesto que este problema es soluble, y C++ proporciona un método para resolverlo llamado Herencia Virtual. Sin embargo, el lector versado podría decir: "en cualquier caso, el tener una clase como la D anterior implica que hay un mal diseño". Y el mismo lector tendría la boca llena de razón.
Veamos pués un ejemplo más común. Supongamos que yo quiero que una clase defina un comportamiento en particular. No me interesa lo que haga o cómo lo haga, solo quiero que tenga el comportamiento que quiero. En otras palabras, quiero que una clase tenga ciertos métodos con una firma determinada, pero no quiero hacer una implementación. Este tipo de diseño es común cuando queremos forzar a un objeto a que se ciña a un "contrato" en particular, y es lo que en lenguajes modernos llamamos interfaz. Una interfaz, pués, no es otra cosa que una clase que no tiene variables miembro y que todos los métodos que tiene son virtualmente puros. Algo como:
class ISumable
{
public:
virtual ~ISumable() { }
virtual int Sumar(int a, int b) = 0;
virtual int Restar(int a, int b) = 0;
};
class OperacionesSimples : public ISumable
{
public:
virtual int Sumar(int a, int b) { return a + b; }
virtual int Restar(int a, int b) { return Sumar(a, -b); }
};
Declaramos una interfaz
ISumable que nos garantiza que la clase que la implemente va a poder realizar operaciones aritméticas triviales como la suma y la resta. De esta forma, yo podría crear una función genérica que opere sobre ésta y haga alguna transformación, digamos:
void SumarDiezDígitos(ISumable* pSumable)
{
int valor;
assert(pSumable != NULL);
valor = 0;
for (int i = 0; i < 10; i++)
{
valor = pSumable->(i, valor);
}
cout << "El valor final es: " << valor << endl;
}
No me importa cómo haga la suma, mientras me diga que va a sumar. Así, tenemos una primera clase,
OperacionesSimples que va a implementar esta interfaz. Ahora supongamos que queremos tener otra interfaz que nos permita realizar sumas, restas, multiplicaciones y divisiones. Se vería como:
class IAritmetica
{
public:
virtual ~IAritmetica();
virtual int Suma(int a, int b) = 0;
virtual int Resta(int a, int b) = 0;
virtual int Multiplicacion(int a, int b) = 0;
virtual int Division(int a, int b) = 0;
};
Y queremos crear una clase que implemente dicha interfaz, pero para reutilizar código, derivamos de nuestra clase
OperacionesSimples. Entonces tendríamos:
class OperacionesAritmeticas : public OperacionesSimples, public IAritmetica
{
public:
virtual int Multiplicar(int a, int b) { return a * b; }
virtual int Dividir(int a, int b) { return b != 0 ? a / b : 0; }
};
Lo anterior no compilará porque tanto
IAritmetica como
OperacionesSimples definen dos métidos iguales:
Sumar y
Restar. ¿Qué podemos hacer ante esto? Evidentemente, la solución a ambos problemas reside en la herencia virtual.
La herencia virtual en esencia es un mecanismo a través del cuál le decimos al compilador que herede de una clase pero que haga los métodos heredados
virtualmente puros. En consecuencia, tendremos que implementarlos
aun si la clase base los implementa. Para hacer esto, empleamos la palabra reservada
virtual al heredar de nuestra clase. La resolución, pues, del primer problema planteado quedaría como sigue.
class A
{
public:
virtual string Foo() { return "A::Foo"; }
};
class B : public A
{
public:
virtual string Foo() { return "B::Foo"; }
};
class C : public A
{
public:
virtual string Foo() { return "C::Foo"; }
};
class D : public virtual B, public virtual C
{
public:
virtual string Foo() { return B::Foo(); }
};
int main()
{
D d;
d.Foo();
return EXIT_SUCCESS;
}
Ahora sí no habrá problema de compilación, ya que heredamos D virtualmente tanto de B como de C. Por supuesto, tuvimos que implementar el método para que explícitamente decida cuál versión llamar, o bien escriba una versión propia del método.
La resolución del segundo problema quedaría como sigue.
class OperacionesSimples : public virtual ISumable
{
public:
virtual int Sumar(int a, int b) { return a + b; }
virtual int Restar(int a, int b) { return Sumar(a, -b); }
};
class OperacionesAritmeticas : public OperacionesSimples, public virtual IAritmetica
{
public:
virtual int Sumar(int a, int b) { return OperacionesSipmles::Sumar(a, b); }
virtual int Restar(int a, int b) { return OperacionesSimples::Restar(a, b); }
virtual int Multiplicar(int a, int b) { return a * b; }
virtual int Dividir(int a, int b) { return b != 0 ? a / b : 0; }
};
Y voilà, se acabó el problema. La herencia virtual es bastante útil sobre todo cuando diseñamos e implementamos interfases. De hecho es recomendable que todas nuestas interfases las heredemos de forma virtual, para evitarnos problemas relacionados con la herencia múltiple, la cuál como se ha demostrado, solo genera problemas cuando no se conoce el lenguaje.