Ir al contenido

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(arg) 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.

raii_file_descriptor.cpp
#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.

Error común

1. Reenviar mas de una vez — std::forward(arg) 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(x)); call_b(std::forward(x)); 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.

Buena práctica

El reenvio perfecto se usa en:

1. Funciones fabrica — make_unique(args...) 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(args)...); }

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.

Puntos clave
  • Las referencias de reenvio (T&& con T deducido) preservan la categoria de valor del argumento
  • std::forward es 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: template void f(T&& arg) { g(std::forward(arg)); }
  • auto&& es una referencia de reenvio fuera de plantillas
  • decltype(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`?