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));
}
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_cast
es 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 f
con 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 A
por 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 forward
que 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 aconst int
. Los errores son para los literales rvalues. También tenga en cuenta que para la llamada adeduced(1)
x esint&&
noint
(el reenvío perfecto nunca hace una copia, como se haría six
sería un parámetro por valor). MeramenteT
esint
. La razón quex
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
omove
¿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 questd::forward
puede terminar como cualquiera. Usarstd::move
cuando sepa que ya no necesita el valor y desea moverlo a otro lugar, usestd::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
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
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
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 char
y 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.
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
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::forward
nos 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 adecltype(x)(x)
(asumiendox
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
Observación: También puedes escribir
std::forward<decltype(t1)>(t1)
odecltype(t1)(t1)
, vea c ++ – ¿Reenvío perfecto en un lambda? – Desbordamiento de pila– usuario202729
22 de enero a las 13:32