En C++, el polimorfismo dinámico se implementa a través del mecanismo de funciones virtuales mediante una tabla especial — vtable (tabla virtual). Para cualquier clase que contenga al menos una función virtual, el compilador genera una vtable: es un arreglo de punteros a las funciones virtuales de la clase. Cada objeto de dicha clase almacena un puntero oculto a la vtable — el llamado vptr (puntero virtual).
En el momento de crear un objeto de una clase con funciones virtuales, el vptr se inicializa con la tabla vtable correspondiente a la clase del objeto. Al llamar a una función virtual, la llamada no se realiza directamente, sino a través de la dirección almacenada en la vtable, seleccionando la implementación correcta dependiendo del tipo real del objeto (incluso al acceder mediante un tipo base).
class Base { public: virtual void foo() { std::cout << "Base::foo() "; } }; class Derived : public Base { public: void foo() override { std::cout << "Derived::foo() "; } }; void call_foo(Base* obj) { obj->foo(); // llamada a través de vtable }
La llamada a foo será un acceso a la vtable: si es en Derived, se llamará a la función sobreescrita.
"¿Es un método virtual si se declara como
virtual, pero no se sobreescribe en la clase derivada? ¿Funciona en este caso la llamada a través de vtable?"
Respuesta: Sí, si la función se declara como virtual en la clase base, permanece virtual para todos los descendientes, incluso si no se sobreescribe. La llamada a través del puntero base de todos modos pasará por la vtable. Si el método no se sobreescribe, se ejecutará la versión de la clase base.
Ejemplo:
struct Base { virtual void foo() { std::cout << "B "; } }; struct Derived : Base { }; Base* obj = new Derived(); obj->foo(); // Imprimirá "B", ¡pero la llamada es a través de vtable!
Historia
En un proyecto grande, se olvidaron de declarar el destructor como virtual en la clase base con funciones virtuales. Esto llevó a fugas de memoria: al eliminar a través de un puntero base, solo se llamaba al destructor de la clase base, no al del descendiente.
Historia
En la clase se declaró un método virtual que por error fue ocultado en el heredado (no
override, sino con una firma/nombre de argumentos diferente). Las llamadas a través de la clase base no llegaban a la función correcta, lo que provocaba un comportamiento incorrecto.
Historia
La actualización de la vtable después de la herencia múltiple llevó a errores: llamadas incorrectas a métodos al usar dynamic_cast, ya que no se tuvo en cuenta el desplazamiento del vptr en una jerarquía de clases compleja.