Polimorfismo: Compilación vs. Tiempo de ejecución
Parte de la sección Programación Orientada a Objetos del Journey de C++ de Coddy. Lección 56 de 104.
El polimorfismo significa «muchas formas» y es un concepto fundamental de la POO que permite tratar los objetos de manera uniforme mientras se comportan de forma diferente. C++ admite dos tipos distintos de polimorfismo, cada uno de los cuales se resuelve en una etapa diferente de la ejecución del programa.
Polimorfismo en tiempo de compilación (también llamado polimorfismo estático) se resuelve mediante el compilador antes de que se ejecute el programa. El compilador determina exactamente qué función llamar basándose en la firma de la función. Esto incluye la sobrecarga de funciones y las plantillas:
void print(int x) { std::cout << "Integer: " << x << std::endl; }
void print(double x) { std::cout << "Double: " << x << std::endl; }
print(5); // El compilador elige print(int)
print(3.14); // El compilador elige print(double)El polimorfismo en tiempo de ejecución (también llamado polimorfismo dinámico) se resuelve mientras el programa se está ejecutando. La decisión sobre qué función llamar depende del tipo real del objeto, no del tipo del puntero o de la referencia. Esto se logra mediante funciones virtuales:
class Shape {
public:
virtual void draw() { std::cout << "Drawing shape" << std::endl; }
};
class Circle : public Shape {
public:
void draw() override { std::cout << "Drawing circle" << std::endl; }
};
Shape* s = new Circle();
s->draw(); // Decidido en tiempo de ejecución: "Drawing circle"El compromiso clave: el polimorfismo en tiempo de compilación no tiene ninguna sobrecarga en tiempo de ejecución, ya que las decisiones se toman durante la compilación, mientras que el polimorfismo en tiempo de ejecución añade un pequeño coste (búsqueda en la vtable), pero ofrece una mayor flexibilidad para trabajar con objetos cuyos tipos no se conocen hasta la ejecución.
Desafío
FácilConstruyamos un sistema de calculadora que demuestre ambos tipos de polimorfismo uno al lado del otro. Crearás un sistema en el que el polimorfismo en tiempo de compilación gestione diferentes tipos de entrada mediante la sobrecarga de funciones, mientras que el polimorfismo en tiempo de ejecución permita intercambiar dinámicamente distintas estrategias de cálculo.
Organizarás tu código en tres archivos:
Calculator.h: Define una clase baseCalculatorque represente cualquier estrategia de cálculo:- Un método virtual
calculate(int a, int b)que devuelva uninte imprima:Base calculation: <a> ? <b>(devolviendo 0) - Un destructor virtual
- Un método virtual
Operations.h: Define dos clases de calculadora derivadas que sobrescriban el comportamiento del cálculo:Adder: Sobrescribecalculate()para imprimirAdding: <a> + <b>y devolver la sumaMultiplier: Sobrescribecalculate()para imprimirMultiplying: <a> * <b>y devolver el producto
override.main.cpp: Crea un sistema que muestre ambos tipos de polimorfismo. Lee dos entradas enteras (cada una en una línea separada).Primero, demuestra el polimorfismo en tiempo de compilación creando tres funciones
display()sobrecargadas:display(int x)imprime:Integer value: <x>display(double x)imprime:Double value: <x>display(const std::string& x)imprime:String value: <x>
Después, demuestra el polimorfismo en tiempo de ejecución creando un arreglo de punteros
Calculator*que contenga unCalculatorbase, unAddery unMultiplier. Recorre el arreglo y llama acalculate()en cada elemento con tus valores de entrada, imprimiendo el resultado después de cada cálculo.Estructura tu salida de la siguiente manera:
=== Compile-Time Polymorphism === <display outputs for int, double, string> === Runtime Polymorphism === <calculate outputs with results>Para la sección de tiempo de compilación, llama a
display()con la primera entrada como un entero, después como un double (el mismo valor con .5 añadido) y después como la cadena "Result". Libera tus calculadoras asignadas dinámicamente cuando termines.
Por ejemplo, con las entradas 10 y 3:
=== Compile-Time Polymorphism ===
Integer value: 10
Double value: 10.5
String value: Result
=== Runtime Polymorphism ===
Base calculation: 10 ? 3
Result: 0
Adding: 10 + 3
Result: 13
Multiplying: 10 * 3
Result: 30Observa cómo el compilador selecciona la sobrecarga correcta de display() según el tipo del argumento (decisión en tiempo de compilación), mientras que el método calculate() correcto se determina según el tipo real del objeto en tiempo de ejecución mediante el mecanismo de vtable.
Pruébalo tú mismo
#include <iostream>
#include <string>
#include "Calculator.h"
#include "Operations.h"
// TODO: Crea tres funciones display() sobrecargadas:
// 1. display(int x) - prints "Integer value: <x>"
// 2. display(double x) - imprime "Double value: <x>"
// 3. display(const std::string& x) - imprime "String value: <x>"
int main() {
// Lee dos entradas enteras
int a, b;
std::cin >> a;
std::cin >> b;
// === Compile-Time Polymorphism ===
std::cout << "=== Compile-Time Polymorphism ===" << std::endl;
// TODO: Llama a display() con:
// - a como un entero
// - a como un double (súmale 0.5)
// - la cadena "Result"
std::cout << std::endl;
// === Runtime Polymorphism ===
std::cout << "=== Runtime Polymorphism ===" << std::endl;
// TODO: Crea un array de punteros Calculator* con 3 elementos:
// - a base Calculator
// - un Adder
// - un Multiplier
// TODO: Recorre el array, llama a calculate(a, b) en cada uno,
// e imprime "Result: <return_value>" después de cada cálculo
// TODO: Liberar la memoria asignada dinámicamente
return 0;
}
Esta lección incluye un breve cuestionario. Empieza la lección para responderlo y registrar tu progreso.
Todas las lecciones de Programación Orientada a Objetos
1Fundamentos de OOP
Archivos externosConstrucción y compilación en C++Archivos de cabecera y archivos fuenteNamespaces y alcanceIntroducción a OOP en C++Clases vs ObjetosEl puntero 'this'Métodos (Funciones miembro)Atributos (Miembros de datos)Conceptos básicos de Ctors y DtorsResumen - Calculadora simple4Propiedades de clase
Miembros de instancia vs. estáticosGetters y SettersFunciones miembro constPalabra clave mutableMétodos y variables estáticosFunciones y clases amigasResumen - Gestor de cuentas bancarias7Herencia
Herencia básicaNiveles de acceso en la herenciaOrden de llamada de Ctor y DtorSobrescritura de métodosFunciones virtuales y VTableHerencia múltipleHerencia virtualResumen - Jerarquía de empleados2Gestión de memoria
Memoria Stack vs HeapPunteros y referenciasMemoria dinámica (new/delete)Punteros inteligentes en C++RAII en C++Resumen - Gestor de arrays dinámicos5Encapsulamiento
Especificadores de acceso en C++Especificadores de acceso en profundidadOcultamiento de informaciónStruct vs ClassClases anidadas e internasResumen - Sistema de registros de estudiantes8Polimorfismo
Polimorfismo: Compilación vs. Tiempo de ejecuciónSobrecarga de funcionesFunciones virtuales revisadasFunciones virtuales purasClases abstractasDiseño de interfaces en C++Dynamic Casting y RTTIResumen: Calculadora de figuras11Conceptos avanzados de POO
Composición vs. HerenciaMixins mediante CRTPIdioma PimplBorrado de tiposEnum Classes y tipado fuerteManejo de excepciones en POOJerarquías de excepciones personalizadas14Patrones de diseño - Parte 2
Patrón CommandPatrón AdapterPatrón DecoratorPatrón Template MethodPatrón StatePatrón CompositeRAII como patrón3Constructores y Destructores
Constructor por defectoConstructor parametrizadoConstructor de copiaConstructor de movimientoListas de inicialización del constructorConstructores delegadosAnálisis profundo del destructorRegla de tres / cinco / ceroResumen - Clase String6Sobrecarga de operadores
Introducción a la sobrecarga de operadoresSobrecarga de operadores aritméticosSobrecarga de operadores de comparaciónOperadores de flujo (Stream)Sobrecarga del operador de asignaciónSobrecarga de los operadores [] y ()Operadores de conversión de tiposResumen - Clase Matrix9Plantillas
Plantillas de funcionesPlantillas de clasesEspecialización de plantillasPlantillas variádicasConceptos básicos de SFINAE y Type TraitsResumen - Contenedor genéricoPractica por tu cuenta: Compilador de C++ online