Por qué la herencia por sí sola no basta
En la página anterior construiste una jerarquía de clases: una clase derivada hereda los miembros de su base. Pero hay un detalle. Cuando llamas a un método a través de un puntero a la clase base, C++ decide qué función ejecutar según el tipo del puntero, no según el tipo real del objeto. Así, un Animal* que en realidad apunta a un Dog sigue llamando a la versión del método de Animal.
Eso casi nunca es lo que quieres. La mayoría de las veces tienes una colección de punteros a la clase base, cada uno apuntando a un objeto derivado distinto, y quieres que cada uno se comporte como lo que es. Las funciones virtuales hacen que eso ocurra.
El objeto es realmente un Dog, y aun así a->speak() ejecutó Animal::speak(). Como speak no es virtual, el compilador eligió la función en tiempo de compilación a partir del tipo estático Animal*. Este es el error que las funciones virtuales existen para solucionar.
Hacer que una función sea virtual
Añade la palabra clave virtual al método de la clase base. Ahora la llamada se resuelve en tiempo de ejecución según el tipo real del objeto: esto es el dynamic dispatch.
Un solo bucle sobre Animal*, tres comportamientos distintos. El puntero a la base "conoce" el tipo real en tiempo de ejecución y despacha en consecuencia. Este único mecanismo - una interfaz, muchas implementaciones - es lo que significa el polimorfismo en C++.
Ten en cuenta que virtual solo necesita aparecer en la declaración de la base; una vez que una función es virtual, sigue siendo virtual automáticamente en todas las clases derivadas. Volver a escribirlo en la clase derivada es opcional y redundante.
Usa siempre la palabra clave override
En el ejemplo anterior, cada método derivado está marcado con override. Es opcional para que el código funcione, pero deberías tratarlo como obligatorio. override (C++11) le pide al compilador que verifique que realmente estás sobrescribiendo una función virtual de la base con una firma coincidente. Si te equivocas sutilmente en la firma, obtienes un error claro en lugar de un fallo silencioso.
struct Animal {
virtual void speak() const { } // ojo: const
};
struct Dog : Animal {
void speak() { } // NO es const - esto es una función NUEVA, no una sobrescritura!
void speak() override { } // error: 'speak' no sobrescribe - te lo dice de inmediato
};
Sin override, el primer speak() compila sin problemas pero nunca se llama a través de un Animal*, porque su firma difiere de la de la base (le falta const). Pasarías una tarde entera preguntándote por qué tu sobrescritura no hace nada. Con override, el compilador detecta la discordancia al instante. Añádelo a cada función que sobrescribe.
Funciones virtuales puras y clases abstractas
A veces la clase base no tiene un valor por defecto razonable: ¿qué sonido hace un "Animal" genérico? En ese caso, declara la función como virtual pura asignándole = 0. Esto la deja sin cuerpo y convierte la clase en una clase abstracta que no puede instanciarse por sí sola. Solo existe para definir una interfaz que las clases derivadas deben cumplir.
Cada subclase concreta debe implementar area(), o también se queda abstracta. Así es como C++ expresa las "interfaces": una clase abstracta con solo funciones virtuales puras es el equivalente en C++ de una interfaz en lenguajes como Java.
La regla del destructor virtual
Esta es la trampa que pilla a todo el mundo al menos una vez. Cuando haces delete de un objeto a través de un puntero a la clase base, C++ llama al destructor que encuentra, y si ese destructor no es virtual, solo ejecuta el destructor de la base. La parte derivada nunca se destruye, perdiendo todo lo que poseía. El estándar llama a esto comportamiento indefinido.
La solución es una sola palabra: haz que el destructor de la base sea virtual. Entonces delete p ejecuta ~Derived primero y luego ~Base, exactamente como debería.
struct Base {
virtual ~Base() { cout << "~Base\n"; } // correcto
};
// ahora: ~Derived y luego ~Base
Regla práctica: en el momento en que una clase tenga cualquier función virtual, dale también un destructor virtual. Si una clase está pensada para ser una clase base usada a través de punteros, su destructor debe ser virtual.
Errores comunes y trampas
Algunas trampas más a las que estar atento una vez que te sientas cómodo con las funciones virtuales:
Object slicing (recorte de objetos). Si pasas o almacenas un objeto derivado por valor en una variable de la base, la parte derivada queda "recortada" y te quedas con un objeto base simple: el despacho virtual ya no alcanza la sobrescritura. Usa siempre punteros o referencias para el polimorfismo:
Dog d;
Animal a = d; // RECORTADO: a ahora es solo un Animal, la parte Dog desapareció
a.speak(); // ejecuta Animal::speak aunque sea virtual
Animal& ref = d; // OK: la referencia conserva el tipo real
ref.speak(); // ejecuta Dog::speak
No llames a funciones virtuales desde constructores o destructores. Durante la construcción la parte derivada aún no existe, así que una llamada virtual se resuelve a la versión de la clase actual, no a la sobrescritura derivada, lo que rara vez es lo que pretendes.
El despacho virtual tiene un pequeño coste. Cada llamada virtual pasa por una tabla oculta de punteros a funciones (la "vtable"), una indirección por llamada. Es barato, pero no gratis, así que no hagas virtual una función a menos que realmente necesites sobrescribirla.
Llamar a la versión de la base a propósito. Dentro de una sobrescritura todavía puedes invocar la implementación de la base explícitamente con Base::method(): útil cuando el comportamiento derivado amplía en lugar de reemplazar al de la base.
Siguiente: Sobrecarga de operadores
Las funciones virtuales permiten que tus objetos personalicen su comportamiento a través de una interfaz compartida. La siguiente página muestra cómo personalizar los operadores que actúan sobre tus objetos: con la sobrecarga de operadores puedes enseñar a tus propios tipos a responder a +, ==, << y más, de modo que Vector + Vector o cout << myObject se lean de forma tan natural como con los tipos integrados.
Preguntas frecuentes
¿Qué es una función virtual en C++?
Una función virtual es una función miembro declarada con la palabra clave virtual en una clase base, de modo que cuando la llamas a través de un puntero o una referencia a la clase base, C++ ejecuta la versión sobrescrita de la clase derivada en lugar de la de la base. Esta selección en tiempo de ejecución se llama dynamic dispatch (despacho dinámico) y es la base del polimorfismo.
¿Cuál es la diferencia entre una función virtual y una función virtual pura?
Una función virtual tiene cuerpo y puede sobrescribirse. Una función virtual pura se declara con = 0 y no tiene cuerpo en la clase base: obliga a que cada clase derivada concreta proporcione una implementación. Cualquier clase con al menos una función virtual pura es una clase abstracta y no se puede instanciar.
¿Por qué una clase base necesita un destructor virtual en C++?
Si haces delete de un objeto derivado a través de un puntero a la clase base y el destructor de la base no es virtual, solo se ejecuta el destructor de la base: la parte derivada nunca se limpia, lo que provoca fugas de recursos y es comportamiento indefinido. Declara virtual el destructor de cualquier clase pensada para usarse de forma polimórfica.