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 los pasan a otras funciones, surge un problema: ¿cómo se preserva si el argumento original era un lvalue o un rvalue? El reenvío perfecto (perfect forwarding) resuelve esto pasando los argumentos exactamente como se recibieron, manteniendo su categoría de valor.
Los ingredientes clave son las referencias de reenvío (forwarding references) (escritas como T&& en un contexto de plantilla) y std::forward. Una referencia de reenvío puede vincularse tanto a lvalues como a rvalues, y std::forward realiza una conversión (cast) condicional del argumento 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 son siempre lvalues dentro de la función, incluso si se pasó un rvalue. Esto evitaría que la semántica de movimiento funcione correctamente al realizar el reenvío a constructores u otras funciones.
El reenvío perfecto (perfect forwarding) es esencial para escribir funciones de fábrica (factory functions), clases envolventes (wrapper classes) y cualquier código genérico que necesite construir objetos o invocar funciones preservando la intención del llamador con respecto a los movimientos frente a las copias.
Desafío
FácilVamos a construir una función de fábrica que demuestre el reenvío perfecto (perfect forwarding) construyendo objetos mientras se preserva la categoría de valor de los argumentos que se le pasan. Crearás una función genérica make_object que reenvía argumentos al constructor de una clase, asegurando que los lvalues se copien y los rvalues se muevan.
Organizarás tu código en tres archivos:
Widget.h: Define una claseWidgetque rastrea cómo recibe sus datos.Tu
Widgetdebe almacenar un nombrestd::stringy un valorint. Proporciona dos constructores:- Uno que tome
const std::string&(referencia lvalue) eint— imprimeWidget constructed (copy): [name] - Uno que tome
std::string&&(referencia rvalue) eint— imprimeWidget constructed (move): [name]y mueve la cadena
También añade un método
display()que imprima[name]: [value].- Uno que tome
Factory.h: Crea tu función de fábrica de reenvío perfecto.Escribe una función de plantilla
make_objectque acepte una referencia de reenvío (forwarding reference) para el nombre y unintregular para el valor. Usastd::forwardpara pasar el argumento del nombre al constructor deWidgetpreservando su categoría de valor original.La función debe devolver el
Widgetconstruido por valor.main.cpp: Lee dos entradas:- Un nombre (string)
- 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 - Segundo, pasa una cadena temporal (el mismo nombre concatenado con
"_temp") directamente amake_objectcomo 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, preserva esa información al llamar al constructor de Widget. Esto permite que la resolución de sobrecarga del constructor seleccione la versión adecuada, permitiendo movimientos eficientes para temporales mientras se copian de forma segura las variables con nombre.
Hoja de referencia
El reenvío perfecto (perfect forwarding) permite que las funciones de plantilla pasen argumentos a otras funciones preservando su categoría de valor original (lvalue o rvalue).
El mecanismo utiliza referencias de reenvío (T&& en el contexto de la plantilla) combinadas con std::forward:
#include <utility>
template<typename T>
void wrapper(T&& arg) {
process(std::forward<T>(arg));
}Una referencia de reenvío puede vincularse tanto a lvalues como a rvalues. std::forward<T> realiza un cast condicional del argumento de vuelta a su tipo original.
Sin std::forward, los parámetros con nombre dentro de una función siempre se tratan como lvalues, incluso si se pasó un rvalue. Esto evitaría que la semántica de movimiento funcione correctamente.
Ejemplo que demuestra la preservación de la categoría de valor:
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 n = 10;
wrapper(n); // Llama a process(int&) - lvalue preservado
wrapper(20); // Llama a process(int&&) - rvalue preservadoEl reenvío perfecto es esencial para funciones de fábrica, clases envolventes y código genérico que construye objetos o invoca funciones preservando la intención del llamador con respecto a movimientos frente a copias.
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: Crear el primer widget pasando name como un lvalue
// Almacenar el name en una variable local y pasarla a make_object
// TODO: Llamar a display() en el primer widget
// TODO: Crear el segundo widget pasando una cadena temporal como un rvalue
// Pasar (name + "_temp") directamente a make_object
// TODO: Llamar 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, any