¿Cuáles son los propósitos principales de std::forward y qué problemas resuelve?

15 minutos de lectura

Avatar de usuario de Steveng
steven

En perfecto reenvío, std::forward se utiliza para convertir las referencias rvalue con nombre t1 y t2 a referencias rvalue sin nombre. ¿Cuál es el propósito de hacer eso? ¿Cómo afectaría eso a la función llamada? inner si nos vamos t1 & t2 como valores?

template <typename T1, typename T2>
void outer(T1&& t1, T2&& t2) 
{
    inner(std::forward<T1>(t1), std::forward<T2>(t2));
}

  • Observación: También puedes escribir std::forward<decltype(t1)>(t1) o decltype(t1)(t1), vea c ++ – ¿Reenvío perfecto en un lambda? – Desbordamiento de pila

    – usuario202729

    22 de enero a las 13:32

Avatar de usuario de GManNickG
GManNickG

Tienes que entender el problema del reenvío. Puedes leer todo el problema en detallepero voy a resumir.

Básicamente, dada la expresión E(a, b, ... , c)queremos la expresión f(a, b, ... , c) ser equivalente. En C++03, esto es imposible. Hay muchos intentos, pero todos fallan en algún aspecto.


El más simple es usar una referencia lvalue:

template <typename A, typename B, typename C>
void f(A& a, B& b, C& c)
{
    E(a, b, c);
}

Pero esto no puede manejar valores temporales (rvalues): f(1, 2, 3);ya que no se pueden vincular a una referencia de lvalue.

El siguiente intento podría ser:

template <typename A, typename B, typename C>
void f(const A& a, const B& b, const C& c)
{
    E(a, b, c);
}

Lo que soluciona el problema anterior porque “const X& se une a todo“, incluidos los valores l y r, pero esto provoca un nuevo problema. Ahora no permite E tener no-const argumentos:

int i = 1, j = 2, k = 3;
void E(int&, int&, int&); 
f(i, j, k); // oops! E cannot modify these

El tercer intento acepta referencias constantes, pero luego const_castes el const lejos:

template <typename A, typename B, typename C>
void f(const A& a, const B& b, const C& c)
{
    E(const_cast<A&>(a), const_cast<B&>(b), const_cast<C&>(c));
}

Esto acepta todos los valores, puede transmitir todos los valores, pero potencialmente conduce a un comportamiento indefinido:

const int i = 1, j = 2, k = 3;
E(int&, int&, int&); 
f(i, j, k); // ouch! E can modify a const object!

Una solución final maneja todo correctamente… a costa de ser imposible de mantener. Usted proporciona sobrecargas de fcon todos combinaciones de constantes y no constantes:

template <typename A, typename B, typename C>
void f(A& a, B& b, C& c);

template <typename A, typename B, typename C>
void f(const A& a, B& b, C& c);

template <typename A, typename B, typename C>
void f(A& a, const B& b, C& c);

template <typename A, typename B, typename C>
void f(A& a, B& b, const C& c);

template <typename A, typename B, typename C>
void f(const A& a, const B& b, C& c);

template <typename A, typename B, typename C>
void f(const A& a, B& b, const C& c);

template <typename A, typename B, typename C>
void f(A& a, const B& b, const C& c);

template <typename A, typename B, typename C>
void f(const A& a, const B& b, const C& c);

N argumentos requieren 2norte combinaciones, una pesadilla. Nos gustaría hacer esto automáticamente.

(Esto es efectivamente lo que hacemos que el compilador haga por nosotros en C++ 11).


En C++ 11, tenemos la oportunidad de arreglar esto. Una solución modifica las reglas de deducción de plantillas en los tipos existentes, pero esto potencialmente rompe una gran cantidad de código. Así que tenemos que encontrar otra manera.

La solución es utilizar en su lugar el recién agregado rvalue-referencias; podemos introducir nuevas reglas al deducir tipos de referencia de valor-r y crear cualquier resultado deseado. Después de todo, ahora no podemos descifrar el código.

Si se da una referencia a una referencia (obsérvese que la referencia es un término que abarca tanto T& y T&&), usamos la siguiente regla para averiguar el tipo resultante:

“[given] un tipo TR que es una referencia a un tipo T, un intento de crear el tipo “referencia lvalue a cv TR” crea el tipo “referencia lvalue a T”, mientras que un intento de crear el tipo “referencia rvalue a cv TR” crea el tipo TR”.

O en forma tabular:

TR   R

T&   &  -> T&  // lvalue reference to cv TR -> lvalue reference to T
T&   && -> T&  // rvalue reference to cv TR -> TR (lvalue reference to T)
T&&  &  -> T&  // lvalue reference to cv TR -> lvalue reference to T
T&&  && -> T&& // rvalue reference to cv TR -> TR (rvalue reference to T)

A continuación, con la deducción del argumento de la plantilla: si un argumento es un lvalue A, proporcionamos al argumento de la plantilla una referencia de lvalue a A. De lo contrario, deducimos normalmente. Esto da el llamado referencias universales (el término referencia de reenvío ahora es el oficial).

¿Por qué es útil? Porque combinados mantenemos la capacidad de realizar un seguimiento de la categoría de valor de un tipo: si era un valor l, tenemos un parámetro de referencia de valor l, de lo contrario, tenemos un parámetro de referencia de valor r.

En codigo:

template <typename T>
void deduce(T&& x); 

int i;
deduce(i); // deduce<int&>(int& &&) -> deduce<int&>(int&)
deduce(1); // deduce<int>(int&&)

Lo último es “reenviar” la categoría de valor de la variable. Tenga en cuenta que, una vez dentro de la función, el parámetro podría pasarse como un valor l a cualquier cosa:

void foo(int&);

template <typename T>
void deduce(T&& x)
{
    foo(x); // fine, foo can refer to x
}

deduce(1); // okay, foo operates on x which has a value of 1

Eso no es bueno. ¡E necesita obtener el mismo tipo de categoría de valor que obtuvimos! La solución es esta:

static_cast<T&&>(x);

¿Qué hace esto? Considere que estamos dentro del deduce función, y se nos ha pasado un lvalue. Esto significa T es un A&por lo que el tipo de destino para la conversión estática es A& &&o solo A&. Ya que x ya es un A&no hacemos nada y nos quedamos con una referencia lvalue.

Cuando nos han pasado un rvalue, T es Apor lo que el tipo de destino para la conversión estática es A&&. La conversión da como resultado una expresión rvalue, que ya no se puede pasar a una referencia lvalue. Hemos mantenido la categoría de valor del parámetro.

Poner estos juntos nos da “reenvío perfecto”:

template <typename A>
void f(A&& a)
{
    E(static_cast<A&&>(a)); 
}

Cuando f recibe un lvalue, E obtiene un valor l. Cuando f recibe un valor r, E obtiene un valor r. Perfecto.


Y por supuesto, queremos deshacernos de lo feo. static_cast<T&&> es críptico y raro de recordar; en su lugar, hagamos una función de utilidad llamada forwardque hace lo mismo:

std::forward<A>(a);
// is the same as
static_cast<A&&>(a);

  • no lo haría f ser una función y no una expresión?

    –Michael Foukarakis

    27 de agosto de 2010 a las 8:07


  • Su último intento no es correcto con respecto a la declaración del problema: reenviará los valores constantes como no constantes, por lo que no se reenviará en absoluto. También tenga en cuenta que en el primer intento, el const int i será aceptado: A se deduce a const int. Los errores son para los literales rvalues. También tenga en cuenta que para la llamada a deduced(1)x es int&&no int (el reenvío perfecto nunca hace una copia, como se haría si x sería un parámetro por valor). Meramente T es int. La razón que x se evalúa como un valor l en el reenviador se debe a que las referencias de valor r con nombre se convierten en expresiones de valor l.

    – Johannes Schaub – litb

    27 de agosto de 2010 a las 11:40


  • ¿Hay alguna diferencia en el uso forward o move ¿aquí? ¿O es solo una diferencia semántica?

    – David G.

    1 de abril de 2013 a las 12:42

  • @David: std::move debe llamarse sin argumentos de plantilla explícitos y siempre da como resultado un valor r, mientras que std::forward puede terminar como cualquiera. Usar std::move cuando sepa que ya no necesita el valor y desea moverlo a otro lugar, use std::forward para hacerlo de acuerdo con los valores pasados ​​a su plantilla de función.

    – GManNickG

    01/04/2013 a las 14:52

  • Gracias por empezar primero con ejemplos concretos y motivar el problema; ¡muy útil!

    – ShreevatsaR

    30 de enero de 2015 a las 20:59

avatar de usuario de user7610
usuario7610

Creo que tener un código conceptual que implemente std::forward puede ayudar con la comprensión. Esta es una diapositiva de la charla de Scott Meyers Una muestra efectiva de C++ 11/14

código conceptual que implementa std::forward

Función move en el codigo esta std::move. Hay una implementación (funcional) para esto anteriormente en esa charla. encontré implementación real de std::forward en libstdc++en el archivo move.h, pero no es nada instructivo.

Desde la perspectiva de un usuario, el significado de esto es que std::forward es una conversión condicional a un valor r. Puede ser útil si estoy escribiendo una función que espera un valor l o un valor r en un parámetro y quiere pasarlo a otra función como un valor r solo si se pasó como un valor r. Si no envolviera el parámetro en std::forward, siempre se pasaría como una referencia normal.

#include <iostream>
#include <string>
#include <utility>

void overloaded_function(std::string& param) {
  std::cout << "std::string& version" << std::endl;
}
void overloaded_function(std::string&& param) {
  std::cout << "std::string&& version" << std::endl;
}

template<typename T>
void pass_through(T&& param) {
  overloaded_function(std::forward<T>(param));
}

int main() {
  std::string pes;
  pass_through(pes);
  pass_through(std::move(pes));
}

Efectivamente, imprime

std::string& version
std::string&& version

El código se basa en un ejemplo de la charla mencionada anteriormente. Diapositiva 10, a eso de las 15:00 desde el inicio.

  • Su segundo enlace terminó apuntando a un lugar completamente diferente.

    – Farap

    4 de mayo de 2018 a las 7:01


  • Vaya, gran explicación. Empecé con este video: youtube.com/watch?v=srdwFMZY3Hg, pero después de leer tu respuesta, finalmente lo siento. 🙂

    – flamenco

    15 de noviembre de 2020 a las 21:45


avatar de usuario de sellibitze
vender

En el reenvío perfecto, std::forward se usa para convertir la referencia t1 y t2 de valor r con nombre en una referencia de valor r sin nombre. ¿Cuál es el propósito de hacer eso? ¿Cómo afectaría eso a la función interna llamada si dejamos t1 y t2 como lvalue?

template <typename T1, typename T2> void outer(T1&& t1, T2&& t2) 
{
    inner(std::forward<T1>(t1), std::forward<T2>(t2));
}

Si usa una referencia de valor r con nombre en una expresión, en realidad es un valor l (porque se refiere al objeto por su nombre). Considere el siguiente ejemplo:

void inner(int &,  int &);  // #1
void inner(int &&, int &&); // #2

Ahora bien, si llamamos outer como esto

outer(17,29);

nos gustaría que 17 y 29 se reenviaran al n. ° 2 porque 17 y 29 son literales enteros y, como tales, valores r. Pero desde t1 y t2 en la expresión inner(t1,t2); son lvalues, estaría invocando #1 en lugar de #2. Es por eso que necesitamos volver a convertir las referencias en referencias sin nombre con std::forward. Asi que, t1 en outer es siempre una expresión lvalue mientras que forward<T1>(t1) puede ser una expresión rvalue dependiendo de T1. La última es solo una expresión de valor l si T1 es una referencia de valor l. Y T1 solo se deduce que es una referencia de lvalue en caso de que el primer argumento de outside fuera una expresión de lvalue.

  • Esta es una especie de explicación diluida, pero una explicación muy bien hecha y funcional. Las personas deberían leer esta respuesta primero y luego profundizar si lo desean.

    – Nico Berrogorry

    27 de junio de 2018 a las 21:42

  • @sellibitze Una pregunta más, qué afirmación es correcta al deducir int a;f(a):”dado que a es un valor l, entonces int(T&&) equivale a int(int& &&)” o “para hacer que T&& sea igual a int&, entonces T debería ser int&”? Prefiero a este último.

    – John

    14 de junio de 2020 a las 5:29

¿Cómo afectaría eso a la función interna llamada si dejamos t1 y t2 como lvalue?

Si, después de instanciar, T1 es de tipo chary T2 es de una clase, quieres pasar t1 por copia y t2 por const referencia. Bueno, a menos que inner() los toma por no-const referencia, es decir, en cuyo caso también desea hacerlo.

Trate de escribir un conjunto de outer() funciones que implementan esto sin referencias rvalue, deduciendo la forma correcta de pasar los argumentos de inner()tipo de Creo que necesitará algo 2^2 de ellos, material de plantilla-meta bastante fuerte para deducir los argumentos, y mucho tiempo para hacerlo bien en todos los casos.

Y entonces alguien viene con un inner() que toma argumentos por puntero. Creo que ahora hace 3^2. (O 4 ^ 2. Demonios, no puedo molestarme en tratar de pensar si const el puntero marcaría la diferencia.)

Y luego imagina que quieres hacer esto para cinco parámetros. O siete.

Ahora sabe por qué a algunas mentes brillantes se les ocurrió el “reenvío perfecto”: hace que el compilador haga todo esto por usted.

Avatar de usuario de Bill Chapman
Bill chapman

Un punto que no ha quedado muy claro es que static_cast<T&&> manejas const T& correctamente también.
Programa:

#include <iostream>

using namespace std;

void g(const int&)
{
    cout << "const int&\n";
}

void g(int&)
{
    cout << "int&\n";
}

void g(int&&)
{
    cout << "int&&\n";
}

template <typename T>
void f(T&& a)
{
    g(static_cast<T&&>(a));
}

int main()
{
    cout << "f(1)\n";
    f(1);
    int a = 2;
    cout << "f(a)\n";
    f(a);
    const int b = 3;
    cout << "f(const b)\n";
    f(b);
    cout << "f(a * b)\n";
    f(a * b);
}

Produce:

f(1)
int&&
f(a)
int&
f(const b)
const int&
f(a * b)
int&&

Tenga en cuenta que ‘f’ tiene que ser una función de plantilla. Si solo se define como ‘void f(int&& a)’, esto no funciona.

  • buen punto, por lo que T & & en el reparto estático también sigue las reglas de colapso de referencia, ¿verdad?

    – barney

    16 de agosto de 2016 a las 14:52

avatar de usuario de colin
colin

Puede valer la pena enfatizar que el reenvío debe usarse junto con un método externo con reenvío/referencia universal. Se permite usar forward por sí mismo como las siguientes declaraciones, pero no sirve para nada más que causar confusión. Es posible que el comité estándar quiera desactivar esa flexibilidad, de lo contrario, ¿por qué no usamos static_cast en su lugar?

     std::forward<int>(1);
     std::forward<std::string>("Hello");

En mi opinión, mover y avanzar son patrones de diseño que son resultados naturales después de que se introduce el tipo de referencia de valor r. No debemos nombrar un método asumiendo que se usa correctamente a menos que esté prohibido el uso incorrecto.

  • buen punto, por lo que T & & en el reparto estático también sigue las reglas de colapso de referencia, ¿verdad?

    – barney

    16 de agosto de 2016 a las 14:52

Desde otro punto de vista, cuando se trata de rvalores en una asignación de referencia universal, puede ser deseable conservar el tipo de una variable tal como es. Por ejemplo

auto&& x = 2; // x is int&&
    
auto&& y = x; // But y is int&    
    
auto&& z = std::forward<decltype(x)>(x); // z is int&&

Usando std::forwardnos aseguramos z tiene exactamente el mismo tipo que x.

Es más, std::forward no afecta las referencias de lvalue:

int i;

auto&& x = i; // x is int&

auto&& y = x; // y is int&

auto&& z = std::forward<decltype(x)>(x); // z is int&

Todavía z tiene el mismo tipo que x.

Entonces, volviendo a tu caso, si la función interna tiene dos sobrecargas para int& y int&&quieres pasar variables como z asignación no y una.

Los tipos en el ejemplo se pueden evaluar a través de:

std::cout<<is_same_v<int&,decltype(z)>;
std::cout<<is_same_v<int&&,decltype(z)>;

  • std::forward<decltype(x)>(x) se puede acortar a decltype(x)(x) (asumiendo x es una referencia).

    – Santo Gato Negro

    27/10/2021 a las 21:45


  • @HolyBlackCat, buen punto. sigo std::forward solo por el bien de la discusión.

    – Sorush

    27/10/2021 a las 21:52

¿Ha sido útil esta solución?