Funciones Virtuales, vtables y Polimorfismo
Profundiza en el polimorfismo en tiempo de ejecucion: como funciona el despacho virtual internamente, cuando usar override y final, clases abstractas y RTTI.
Polimorfismo en Tiempo de Ejecucion
Polimorfismo significa "muchas formas" — la misma interfaz se comporta de manera diferente dependiendo del tipo real del objeto. En C++, el polimorfismo en tiempo de ejecucion se logra a traves de funciones virtuales. Cuando llamas a una funcion virtual a traves de un puntero o referencia base, la llamada se despacha a la implementacion real de la clase derivada en tiempo de ejecucion.
Este es el mecanismo que hace la POO poderosa: puedes escribir codigo que opera sobre una interfaz de clase base, y automaticamente funciona correctamente con cualquier clase derivada — incluso las escritas despues de tu codigo.
Como Funcionan las vtables
Cuando una clase tiene funciones virtuales, el compilador crea una vtable (tabla de funciones virtuales) — un array de punteros a funciones. Cada objeto de esa clase contiene un vptr oculto (puntero a la vtable). Cuando se llama a una funcion virtual, el programa sigue el vptr a la vtable, busca el puntero a la funcion y llama a traves de el. Esta indireccion es por lo que las llamadas virtuales tienen una pequena sobrecarga comparada con las llamadas directas.
#include <iostream>
#include <memory>
#include <vector>
class Shape {
public:
virtual ~Shape() = default;
// Pure virtual — no default implementation
virtual double area() const = 0;
// Virtual with default implementation
virtual std::string name() const { return "Shape"; }
};
class Circle : public Shape {
double radius_;
public:
explicit Circle(double r) : radius_{r} {}
double area() const override {
return 3.14159265 * radius_ * radius_;
}
std::string name() const override { return "Circle"; }
};
class Rectangle : public Shape {
double w_, h_;
public:
Rectangle(double w, double h) : w_{w}, h_{h} {}
double area() const override { return w_ * h_; }
std::string name() const override { return "Rectangle"; }
};
// Works with ANY Shape — past, present, or future
void print_info(const Shape& s) {
std::cout << s.name() << ": area = " << s.area() << '\n';
}
int main() {
Circle c{5.0};
Rectangle r{3.0, 4.0};
print_info(c); // Circle: area = 78.5398
print_info(r); // Rectangle: area = 12
// Polymorphic container
std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Circle>(2.0));
shapes.push_back(std::make_unique<Rectangle>(6.0, 3.0));
for (const auto& s : shapes) {
print_info(*s); // virtual dispatch at runtime
}
return 0;
}Los Destructores Virtuales Son Obligatorios
Si una clase tiene cualquier funcion virtual, su destructor debe ser virtual. Sin el, eliminar un objeto derivado a traves de un puntero base causa comportamiento indefinido — el destructor derivado nunca se ejecuta, provocando fugas de recursos.
#include <iostream>
#include <memory>
class Base {
public:
// Without virtual destructor: UNDEFINED BEHAVIOR on delete
// virtual ~Base() = default; // ALWAYS do this!
~Base() { std::cout << "~Base\n"; }
};
class Derived : public Base {
int* data_;
public:
Derived() : data_{new int[1000]} {
std::cout << "Derived: allocated\n";
}
~Derived() {
delete[] data_;
std::cout << "~Derived: freed\n";
}
};
int main() {
Base* ptr = new Derived{};
delete ptr; // Only ~Base runs! ~Derived is NOT called!
// Result: memory leak (data_ never freed)
// Fix: make ~Base() virtual
return 0;
}Palabras Clave override y final
La palabra clave override le dice al compilador que tienes la intencion de sobreescribir una funcion virtual de la clase base. Si la firma no coincide (un error tipografico comun), el compilador lo detecta. La palabra clave final previene la sobreescritura adicional de una funcion o previene la herencia de una clase.
#include <iostream>
class Base {
public:
virtual ~Base() = default;
virtual void process(int x) const { std::cout << "Base: " << x << '\n'; }
virtual void execute() { std::cout << "Base::execute\n"; }
};
class Middle : public Base {
public:
// override: compiler checks signature matches Base::process
void process(int x) const override {
std::cout << "Middle: " << x * 2 << '\n';
}
// final: no further derived class can override execute()
void execute() final { std::cout << "Middle::execute\n"; }
};
class Bottom : public Middle {
public:
void process(int x) const override {
std::cout << "Bottom: " << x * 3 << '\n';
}
// ERROR if uncommented: execute() is final in Middle
// void execute() override { }
};
// A final class cannot be inherited from at all
class Leaf final : public Base {
public:
void process(int x) const override {
std::cout << "Leaf: " << x << '\n';
}
};
// ERROR if uncommented: cannot inherit from final class
// class Attempt : public Leaf {};
int main() {
Bottom b;
Base& ref = b;
ref.process(10); // Bottom: 30 (virtual dispatch)
ref.execute(); // Middle::execute (final version)
return 0;
}Clases Abstractas y Patron NVI
Una clase con al menos una funcion virtual pura (= 0) es abstracta — no puede instanciarse. El patron Non-Virtual Interface (NVI) proporciona un punto de entrada publico no virtual que llama a una funcion virtual privada, permitiendo que la clase base controle las pre/post condiciones.
#include <iostream>
#include <string>
// NVI Pattern: public non-virtual calls private virtual
class Validator {
public:
virtual ~Validator() = default;
// Public non-virtual interface — controls the workflow
bool validate(const std::string& input) const {
if (input.empty()) {
std::cout << "[Validator] Input is empty\n";
return false;
}
// Delegate to derived class implementation
bool result = do_validate(input);
std::cout << "[Validator] " << (result ? "PASS" : "FAIL") << '\n';
return result;
}
private:
// Pure virtual — derived classes MUST implement
virtual bool do_validate(const std::string& input) const = 0;
};
class EmailValidator : public Validator {
bool do_validate(const std::string& input) const override {
return input.find('@') != std::string::npos
&& input.find('.') != std::string::npos;
}
};
class LengthValidator : public Validator {
std::size_t min_len_;
bool do_validate(const std::string& input) const override {
return input.length() >= min_len_;
}
public:
explicit LengthValidator(std::size_t min) : min_len_{min} {}
};
int main() {
EmailValidator email_v;
LengthValidator len_v{5};
email_v.validate("user@example.com"); // PASS
email_v.validate("invalid"); // FAIL
len_v.validate("hi"); // FAIL
len_v.validate("hello!"); // PASS
return 0;
}RTTI y dynamic_cast
RTTI (Informacion de Tipo en Tiempo de Ejecucion) te permite consultar el tipo real de un objeto polimorfico en tiempo de ejecucion. dynamic_cast convierte de forma segura un puntero base a un puntero derivado, retornando nullptr si la conversion falla. Para referencias, un dynamic_cast fallido lanza std::bad_cast.
typeid(obj) retorna una referencia a std::type_info que puede compararse para identificar el tipo en tiempo de ejecucion.
Usa RTTI con moderacion — el uso frecuente de dynamic_cast a menudo senala un problema de diseno. Prefiere funciones virtuales para comportamiento especifico de tipo.
Nunca llames funciones virtuales desde un constructor o destructor. Durante la construccion, el tipo dinamico del objeto es la clase que se esta construyendo actualmente, no la clase derivada final. Una llamada virtual en Base::Base() llamara a Base::method(), no a Derived::method() — incluso si el objeto que se esta creando es un Derived. Esta es una fuente bien conocida de errores que puede ser extremadamente dificil de diagnosticar.
- Las funciones virtuales habilitan el polimorfismo en tiempo de ejecucion via vtables y vptrs
- Cualquier clase con funciones virtuales debe tener un destructor
virtual - Usa siempre
overrideal sobreescribir — detecta errores de firma en tiempo de compilacion - Las funciones virtuales puras (
= 0) hacen abstracta una clase y fuerzan a las clases derivadas a implementarlas - El patron NVI permite a las clases base imponer pre/post condiciones alrededor de los puntos de personalizacion
- Evita llamar funciones virtuales en constructores/destructores — la clase derivada aun no (o ya no) esta completamente construida
Quiz — Pon a prueba tus conocimientos
(20 XP)1. Que sucede si una clase base polimorfica NO tiene un destructor virtual?
2. Que hace la palabra clave `override`?
3. Por que deberias evitar llamar funciones virtuales desde un constructor?