¿Cómo funciona std :: forward?

Posible duplicado:
Ventajas de usar adelante

Sé lo que hace y cuándo usarlo, pero todavía no puedo entender cómo funciona. Sea lo más detallado posible y explique cuándo std::forward sería incorrecto si se le permitiera usar la deducción del argumento de la plantilla.

Parte de mi confusión es esta: “Si tiene un nombre, es un valor l”, si ese es el caso, ¿por qué std::forward comporta de manera diferente cuando paso thing&& x vs thing& x ?

Primero, echemos un vistazo a lo que hace std::forward acuerdo con el estándar:

§20.2.3 [forward] p2

Devoluciones: static_cast(t)

(Donde T es el parámetro de plantilla explícitamente especificado t es el argumento pasado).

Ahora recuerda las reglas de colapso de referencia:

 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) 

(Desvergonzadamente robado de esta respuesta .)

Y luego echemos un vistazo a una clase que quiere emplear un reenvío perfecto:

 template struct some_struct{ T _v; template some_struct(U&& v) : _v(static_cast(v)) {} // perfect forwarding here // std::forward is just syntactic sugar for this }; 

Y ahora una invocación de ejemplo:

 int main(){ some_struct s1(5); // in ctor: '5' is rvalue (int&&), so 'U' is deduced as 'int', giving 'int&&' // ctor after deduction: 'some_struct(int&& v)' ('U' == 'int') // with rvalue reference 'v' bound to rvalue '5' // now we 'static_cast' 'v' to 'U&&', giving 'static_cast(v)' // this just turns 'v' back into an rvalue // (named rvalue references, 'v' in this case, are lvalues) // huzzah, we forwarded an rvalue to the constructor of '_v'! // attention, real magic happens here int i = 5; some_struct s2(i); // in ctor: 'i' is an lvalue ('int&'), so 'U' is deduced as 'int&', giving 'int& &&' // applying the reference collapsing rules yields 'int&' (& + && -> &) // ctor after deduction and collapsing: 'some_struct(int& v)' ('U' == 'int&') // with lvalue reference 'v' bound to lvalue 'i' // now we 'static_cast' 'v' to 'U&&', giving 'static_cast(v)' // after collapsing rules: 'static_cast(v)' // this is a no-op, 'v' is already 'int&' // huzzah, we forwarded an lvalue to the constructor of '_v'! } 

Espero que esta respuesta paso a paso les ayude a ustedes y a otros a comprender cómo funciona el std::forward .

Creo que la explicación de std::forward as static_cast es confusa. Nuestra intuición para un elenco es que convierta un tipo a otro tipo, en este caso sería una conversión a una referencia rvalue. ¡No es! Así que estamos explicando una cosa misteriosa usando otra cosa misteriosa. Este elenco particular está definido por una tabla en la respuesta de Xeo. ¿Pero la pregunta es porqué? Así que aquí está mi entendimiento:

Supongamos que quiero pasarle un std::vector v que se supone que debe almacenar en su estructura de datos como miembro de datos _v . La solución ingenua (y segura) sería copiar siempre el vector en su destino final. Entonces, si estás haciendo esto a través de una función intermediaria (método), esa función debe declararse como tomando una referencia. (Si declara que toma un vector por valor, realizará una copia adicional totalmente innecesaria).

 void set(std::vector & v) { _v = v; } 

Esto está bien si tienes un lvalue en tu mano, pero ¿qué tal un valor r? Supongamos que el vector es el resultado de llamar a una función makeAndFillVector() . Si realizó una tarea directa:

 _v = makeAndFillVector(); 

el comstackdor movería el vector en lugar de copiarlo. Pero si introduce un intermediario, set() , la información sobre la naturaleza rvalue de su argumento se perdería y se haría una copia.

 set(makeAndFillVector()); // set will still make a copy 

Para evitar esta copia, necesita un “reenvío perfecto”, lo que daría como resultado un código óptimo en todo momento. Si le dan un lvalue, quiere que su función sea tratada como un valor l y haga una copia. Si le dan un valor r, desea que su función lo trate como un valor r y lo mueva.

Normalmente lo harías al sobrecargar la función set() por separado para lvalues ​​y rvalues:

 set(std::vector & lv) { _v = v; } set(std::vector && rv) { _v = std::move(rv); } 

Pero imagine que está escribiendo una función de plantilla que acepta T y llama a set() con esa T (no se preocupe por el hecho de que nuestro set() solo está definido para vectores). El truco es que desea que esta plantilla llame a la primera versión de set() cuando la función de plantilla se instancia con un valor l, y la segunda cuando se inicializa con un valor r.

Antes que nada, ¿cuál debería ser la firma de esta función? La respuesta es esta:

 template void perfectSet(T && t); 

Dependiendo de cómo llame a esta función de plantilla, el tipo T se deducirá mágicamente de forma diferente. Si lo llamas con un lvalue:

 std::vector v; perfectSet(v); 

el vector v pasará por referencia. Pero si lo llamas con un valor r:

 perfectSet(makeAndFillVector()); 

el vector (anónimo) se pasará por referencia rvalue. Por lo tanto, la magia de C ++ 11 está configurada a propósito de modo que se preserve la naturaleza rvalua de los argumentos si es posible.

Ahora, dentro de PerfectSet, quiere pasar perfectamente el argumento a la sobrecarga correcta de set() . Aquí es donde std::forward es necesario:

 template void perfectSet(T && t) { set(std::forward(t)); } 

Sin std :: forward el comstackdor debería asumir que queremos pasar t por referencia. Para convencerte de que esto es cierto, compara este código:

 void perfectSet(T && t) { set(t); set(t); // t still unchanged } 

a esto:

 void perfectSet(T && t) { set(std::forward(t)); set(t); // t is now empty } 

Si no reenvía explícitamente t , el comstackdor tiene que asumir de manera defensiva que podría estar accediendo a t nuevamente y elegir la versión de referencia de lvalue del conjunto. Pero si reenvía t , el comstackdor conservará la validez de la misma y se invocará la versión de referencia rvalue de set() . Esta versión mueve el contenido de t , lo que significa que el original se vuelve vacío.

Esta respuesta resultó mucho más larga de lo que inicialmente asumí 😉

Funciona porque cuando se invoca el reenvío perfecto, el tipo T no es el tipo de valor, sino que también puede ser un tipo de referencia.

Por ejemplo:

 template void f(T&&); int main() { std::string s; f(s); // T is std::string& const std::string s2; f(s2); // T is a const std::string& } 

Como tal, forward puede simplemente mirar el tipo explícito T para ver lo que realmente le pasó. Por supuesto, la implementación exacta de hacer esto no es trival, si mal no recuerdo, pero ahí es donde está la información.

Cuando se refiere a una referencia de valor r nombrado , entonces eso es de hecho un valor l. Sin embargo, forward detecta a través de los medios anteriores que en realidad es un valor r, y devuelve correctamente un valor r para ser reenviado.