En muchos lenguajes de programación orientados a objetos, como Java, C#, Python o Ruby, el polimorfismo funciona de forma "natural" al manejar los objetos por referencia. Sin embargo, cuando trabajamos con C++, rápidamente notamos un detalle que desconcierta a muchos: para obtener polimorfismo dinámico necesitamos punteros (o referencias). ¿Por qué ocurre esto? Vamos a desglosarlo.
El polimorfismo permite invocar métodos sobre objetos de clases derivadas a través de una referencia o puntero a la clase base. El método invocado es el de la clase real del objeto, no necesariamente el de la clase base.
En C++, esto se logra declarando métodos virtual en la clase base:
class Animal {
public:
virtual void hablar() {
std::cout << "Soy un animal" << std::endl;
}
};
class Perro : public Animal {
public:
void hablar() override {
std::cout << "Guau" << std::endl;
}
};
Ahora viene la parte clave.
Animal a = Perro();
a.hablar(); // Imprime: "Soy un animal"
A pesar de haber creado un Perro, el método hablar de Animal es el que se llama. ¿Por qué?
Porque en esta asignación:
Animal a = Perro();
se produce slicing ("corte de objeto"). Solo la parte Animal del Perro es copiada en el objeto a. El comportamiento dinámico queda perdido.
Perro p;
Animal* a = &p;
a->hablar(); // Imprime: "Guau"
Ahora sí, el método correcto se invoca. ¿Qué cambió? Simple: no hay slicing. Estamos trabajando con la dirección de memoria del objeto original, donde toda la jerarquía de clases está intacta.
La razón está en el modelo de objetos de C++:
- C++ permite que los objetos vivan por valor. Si asignamos Animal a = Perro(), se copia sólo la parte visible desde Animal. Las partes de Perro no existen en esa copia.
- Los lenguajes como Java o C# manejan los objetos siempre por referencia; nunca se copia el objeto en una asignación como `Animal a = new Perro()`. Por eso ahí el slicing no ocurre.
Cuando usamos punteros o referencias en C++:
- No se copia el objeto.
- Se mantiene la dirección al objeto original.
- El compilador puede usar el vtable (tabla virtual) para despachar la llamada al método correcto.
Una de las ventajas de C++ es que te permite elegir:
- Si quieres performance máxima (sin polimorfismo), puedes trabajar por valor.
- Si quieres polimorfismo dinámico, debes trabajar por puntero o referencia.
Este control es una de las razones por las cuales C++ sigue siendo muy usado en sistemas embebidos, motores de juego, drivers, etc.
C++ también permite otras formas de polimorfismo (estático), como el basado en plantillas (CRTP, SFINAE, concepts en C++20), donde no necesitamos punteros porque el binding ocurre en tiempo de compilación.