Reenvío perfecto
Parte de la sección Programación Orientada a Objetos del Journey de C++ de Coddy. Lección 84 de 104.
Al escribir funciones de plantilla que aceptan argumentos y se los pasan a otras funciones, surge un problema: ¿cómo se conserva si el argumento original era un lvalue o un rvalue? El forwarding perfecto resuelve esto pasando los argumentos exactamente como se recibieron y manteniendo su categoría de valor.
Los ingredientes clave son las referencias de reenvío (escritas como T&& en el contexto de una plantilla) y std::forward. Una referencia de reenvío puede vincularse tanto a lvalues como a rvalues, y std::forward convierte condicionalmente el argumento de nuevo a su tipo original:
#include <iostream>
#include <utility>
void process(int& x) { std::cout << "lvalue: " << x << "\n"; }
void process(int&& x) { std::cout << "rvalue: " << x << "\n"; }
template<typename T>
void wrapper(T&& arg) {
process(std::forward<T>(arg));
}
int main() {
int n = 10;
wrapper(n); // Llama a process(int&) - lvalue preservado
wrapper(20); // Llama a process(int&&) - rvalue preservado
}Sin std::forward, los parámetros con nombre siempre son lvalues dentro de la función, incluso si se pasó un rvalue. Esto impediría que la semántica de movimiento funcionara correctamente al reenviarlos a constructores u otras funciones.
El reenvío perfecto es esencial para escribir funciones de fábrica, clases envoltorio y cualquier código genérico que necesite construir objetos o invocar funciones, preservando al mismo tiempo la intención del autor de la llamada con respecto a los movimientos frente a las copias.
Desafío
FácilConstruyamos una función fábrica que demuestre el reenvío perfecto mediante la construcción de objetos mientras conserva la categoría de valor de los argumentos que se le pasan. Crearás una función genérica make_object que reenvía los argumentos al constructor de una clase, asegurándote de que los lvalues se copien y los rvalues se muevan.
Organizarás tu código en tres archivos:
Widget.h: Define una claseWidgetque registre cómo recibe sus datos.Tu
Widgetdebe almacenar un nombrestd::stringy un valorint. Proporciona dos constructores:- Uno que acepte
const std::string&(referencia a lvalue) yint: imprimeWidget constructed (copy): [name] - Uno que acepte
std::string&&(referencia a rvalue) yint: imprimeWidget constructed (move): [name]y mueve la cadena
Añade también un método
display()que imprima[name]: [value].- Uno que acepte
Factory.h: Crea tu función fábrica de reenvío perfecto.Escribe una función plantilla
make_objectque acepte una referencia de reenvío para el nombre y unintnormal para el valor. Usastd::forwardpara pasar el argumento del nombre al constructor deWidgetmientras conservas su categoría de valor original.La función debe devolver el
Widgetconstruido por valor.main.cpp: Lee dos entradas:- Un nombre (cadena)
- Un valor (entero)
Demuestra el reenvío perfecto creando dos widgets:
- Primero, almacena el nombre en una variable local y pásalo a
make_objectcomo un lvalue - Después, pasa directamente a
"_temp"una cadena temporal (el mismo nombre concatenado conmake_object) como un rvalue
Después de crear cada widget, llama a su método
display().
Por ejemplo, con las entradas Gadget y 42:
Widget constructed (copy): Gadget
Gadget: 42
Widget constructed (move): Gadget_temp
Gadget_temp: 42Con las entradas Device y 100:
Widget constructed (copy): Device
Device: 100
Widget constructed (move): Device_temp
Device_temp: 100La idea clave es que tu función make_object no sabe si recibió un lvalue o un rvalue, pero, al usar std::forward, conserva esa información al llamar al constructor de Widget. Esto permite que la resolución de sobrecarga del constructor seleccione la versión adecuada, habilitando movimientos eficientes para los temporales y copiando de forma segura las variables con nombre.
Pruébalo tú mismo
#include <iostream>
#include <string>
#include "Widget.h"
#include "Factory.h"
using namespace std;
int main() {
// Leer entrada
string name;
int value;
cin >> name >> value;
// TODO: Crea el primer widget pasando name como un lvalue
// Almacena el name en una variable local y pásalo a make_object
// TODO: Llama a display() en el primer widget
// TODO: Crea el segundo widget pasando un string temporal como un rvalue
// Pasa (name + "_temp") directamente a make_object
// TODO: Llama a display() en el segundo widget
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érico12Características de C++ moderno
Semántica de movimiento y RvaluesReenvío perfectoExpresiones Lambda en profundidadstd::function y std::bindconstexpr y constevalBindings estructuradosoptional, variant, anyPractica por tu cuenta: Compilador de C++ online