Reenvio Perfecto y std::forward
Comprende por que las funciones wrapper pierden la categoria de valor, como std::forward la preserva, y el patron idiomatico de reenvio perfecto para plantillas.
El Problema del Reenvio
Considera una funcion wrapper que deberia reenviar sus argumentos a otra funcion exactamente como fueron pasados — si se paso un lvalue, reenvia como lvalue; si se paso un rvalue, reenvia como rvalue.
Con parametros regulares, esto es imposible. Si tomas T&&, una referencia rvalue con nombre se convierte en un lvalue dentro de la funcion. Si tomas const T&, pierdes la posibilidad de mover. Necesitas una forma de preservar la categoria de valor original.
Este es el problema del reenvio, y es resuelto por las referencias de reenvio (T&& donde T es un parametro de plantilla deducido) combinadas con std::forward.
El Patron de Reenvio Perfecto
std::forward es un cast condicional: si T se dedujo como lvalue reference, reenvia como lvalue; si T se dedujo como non-reference, reenvia como rvalue. Es la herramienta que hace que el reenvio perfecto funcione.
#include <unistd.h> // open, close, read, write
#include <fcntl.h> // O_RDONLY, etc.
#include <stdexcept>
#include <utility>
#include <iostream>
class FileDescriptor {
int fd_ = -1; // -1 = no resource
public:
// Acquire: open file in constructor
explicit FileDescriptor(const char* path, int flags)
: fd_(::open(path, flags)) {
if (fd_ < 0) throw std::runtime_error("Cannot open file");
}
// Release: close file in destructor
~FileDescriptor() {
if (fd_ >= 0) ::close(fd_);
}
// Non-copyable (copying a file descriptor is meaningless)
FileDescriptor(const FileDescriptor&) = delete;
FileDescriptor& operator=(const FileDescriptor&) = delete;
// Moveable (transfer ownership)
FileDescriptor(FileDescriptor&& other) noexcept
: fd_(std::exchange(other.fd_, -1)) {}
FileDescriptor& operator=(FileDescriptor&& other) noexcept {
if (this != &other) {
if (fd_ >= 0) ::close(fd_);
fd_ = std::exchange(other.fd_, -1);
}
return *this;
}
int get() const { return fd_; }
};
int main() {
try {
FileDescriptor fd("/tmp/raii_test.txt", O_RDONLY);
std::cout << "File opened: fd=" << fd.get() << '\n';
// Use fd...
} catch (const std::exception& e) {
std::cerr << e.what() << '\n';
}
// fd.~FileDescriptor() called automatically — file closed
// Even if an exception was thrown, the file is closed
}Como Funciona forward Internamente
Cuando una referencia de reenvio T&& se enlaza a un lvalue de tipo U, T se deduce como U&. El colapso de referencias da U& && -> U&. std::forward(arg) retorna U& — un lvalue.
Cuando T&& se enlaza a un rvalue de tipo U, T se deduce como U. No ocurre colapso de referencias: U&&. std::forward(arg) retorna U&& — un rvalue.
Esta es la razon por la que siempre debes usar std::forward con el parametro de plantilla exacto, no std::forward ni un std::move crudo. Usar std::move siempre convertiria a rvalue, rompiendo la reenviabilidad de lvalue.
Plantillas Variadicas y Reenvio
El reenvio perfecto es especialmente poderoso con plantillas variadicas — te permite reenviar cualquier numero de argumentos preservando la categoria de valor de cada uno. Este es el patron usado por std::make_unique, std::make_shared y emplace_back.
1. Reenviar mas de una vez — std::forward no debe usarse mas de una vez para el mismo argumento. Si reenvia como rvalue la primera vez, el segundo uso es un uso despues de mover.
2. Reenviar a multiples llamadores — Relacionado con el anterior, call_a(std::forward es un error. El primer forward puede mover, haciendo la segunda llamada comportamiento indefinido.
3. auto&& en bucles for genéricos — for (auto&& elem : container) es seguro y util — enlaza a tanto lvalues como rvalues. Pero no hagas forward del elem a menos que sepas que el contenedor no lo necesita despues.
4. No mezcles forward y move — std::forward preserva la categoria de valor; std::move siempre produce rvalue. Usar el incorrecto causa copias silenciosas o usos-despues-de-mover.
El reenvio perfecto se usa en:
1. Funciones fabrica — make_unique reenvia args al constructor de T
2. Funciones emplace — vector::emplace_back(args...) construye en-lugar
3. Funciones wrapper/decorador — registro, temporizacion, reintentos
4. Expresiones lambda genéricas — [](auto&&... args) { return f(std::forward
El patron siempre es: plantilla con T&&, forward con std::forward. Desviarse de este patron casi siempre es un error.
auto&& y decltype(auto)
auto&& es una referencia de reenvio que funciona fuera de plantillas. decltype(auto) preserva exactamente la categoria de valor y tipo de referencia de una expresion — util para wrappers de reenvio que necesitan retornar exactamente lo que la funcion envuelta retorna.
- Las referencias de reenvio (
T&&con T deducido) preservan la categoria de valor del argumento std::forwardes un cast condicional — lvalue si T es referencia, rvalue si no lo es- Nunca hagas forward del mismo argumento dos veces — el segundo uso es un uso-despues-de-mover
- El patron:
templatevoid f(T&& arg) { g(std::forward (arg)); } auto&&es una referencia de reenvio fuera de plantillasdecltype(auto)como retorno preserva exactamente el tipo y la categoria de valor de la expresion retornada
Quiz — Pon a prueba tus conocimientos
(15 XP)1. Que hace `std::forward<T>(arg)` cuando T se dedujo como un tipo lvalue reference?
2. Por que no deberias hacer forward del mismo argumento dos veces?
3. Cual es la diferencia entre `std::move` y `std::forward`?