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

Resuelto Steveng asked hace 14 años • 0 respuestas

En reenvío perfecto, std::forwardse utiliza para convertir las referencias de rvalue con nombre t1y t2en referencias de rvalue sin nombre. ¿Cuál es el propósito de hacer eso? ¿ Cómo afectaría eso a la función llamada innersi dejamos t1& t2como valores l?

template <typename T1, typename T2>
void outer(T1&& t1, T2&& t2) 
{
    inner(std::forward<T1>(t1), std::forward<T2>(t2));
}
Steveng avatar Aug 27 '10 13:08 Steveng
Aceptado

Tienes que entender el problema del reenvío. Puedes leer el problema completo en detalle , pero lo resumiré.

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


Lo más sencillo es utilizar una referencia de valor l:

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 pueden vincularse a una referencia lvalue.

El próximo 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 tanto los valores l como los valores r, pero esto causa un nuevo problema. Ahora no permite Etener no constargumentos:

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_castse constacabó:

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 gestiona todo correctamente... a costa de ser imposible de mantener. Proporcionas sobrecargas de f, con todas las combinaciones de constante y no constante:

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 2 N combinaciones, una pesadilla. Nos gustaría hacer esto automáticamente.

(Esto es efectivamente lo que le pedimos al compilador que haga por nosotros en C++ 11).


En C++ 11, tenemos la oportunidad de solucionar este problema. 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. Entonces tenemos que encontrar otra manera.

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

Si se nos da una referencia a una referencia (tenga en cuenta que la referencia es un término que abarca tanto T&como como T&&), usamos la siguiente regla para determinar el tipo resultante:

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

O en forma de tabla:

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 de argumentos de plantilla: si un argumento es un valor l A, proporcionamos al argumento de plantilla una referencia de valor l a A. De lo contrario, deducimos normalmente. Esto da lugar a las llamadas referencias universales (el término referencia de reenvío es ahora el oficial).

¿Por qué es esto ú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; de lo contrario, tenemos un parámetro de referencia de valor.

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 se puede pasar 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 nosotros! La solución es esta:

static_cast<T&&>(x);

¿Qué hace esto? Considere que estamos dentro de la deducefunción y se nos ha pasado un valor l. Esto significa Tque es a A&, por lo que el tipo de objetivo para el lanzamiento estático es A& &&, o simplemente A&. Como xya es un A&, no hacemos nada y nos queda una referencia de valor l.

Cuando se nos ha pasado un rvalue, Tes 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.

Juntarlos nos da un "reenvío perfecto":

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

Cuando frecibe un valor l, Eobtiene un valor l. Cuando frecibe un rvalue, Eobtiene un rvalue. Perfecto.


Y por supuesto, queremos deshacernos de lo feo. static_cast<T&&>es críptico y extraño de recordar; En su lugar, creemos una función de utilidad llamada forward, que hace lo mismo:

std::forward<A>(a);
// is the same as
static_cast<A&&>(a);
GManNickG avatar Aug 27 '2010 07:08 GManNickG

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 An Effective C++11/14 Sampler

código conceptual que implementa std::forward

La función moveen el código es std::move. Hay una implementación (funcional) para esto anteriormente en esa charla. Encontré la implementación real de std::forward en libstdc++ , en el archivo move.h, pero no es nada instructivo.

Desde la perspectiva del usuario, el significado es que std::forwardes una conversión condicional a un rvalue. Puede resultar ú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, aproximadamente a las 15:00 horas del inicio.

user7610 avatar May 01 '2014 15:05 user7610

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

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

Si utiliza 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, si llamamos outerasí

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. Pero como t1y t2en la expresión inner(t1,t2);son valores l, estarías invocando el n.º 1 en lugar del n.º 2. Es por eso que necesitamos volver a convertir las referencias en referencias sin nombre con std::forward. Entonces, t1in outeres siempre una expresión de valor l, mientras que forward<T1>(t1)puede ser una expresión de valor r dependiendo de T1. Esta última es sólo una expresión de valor l si T1es una referencia de valor l. Y T1solo se deduce que es una referencia de valor l en caso de que el primer argumento de exterior fuera una expresión de valor l.

sellibitze avatar Aug 29 '2010 20:08 sellibitze

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

Si, después de crear una instancia, T1es de tipo chary T2es de una clase, desea pasar t1por copia y t2por constreferencia. Bueno, a menos que inner()los tome por no constreferencia, es decir, en cuyo caso usted también querrá hacerlo.

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

Y luego aparece alguien con un argumento inner()que toma argumentos por puntero. Creo que eso ahora hace 3^2. (O 4^2. Demonios, no me molesto en intentar pensar si constel puntero marcaría la diferencia).

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

Ahora sabes por qué a algunas mentes brillantes se les ocurrió el "reenvío perfecto": hace que el compilador haga todo esto por ti.

sbi avatar Aug 27 '2010 07:08 sbi