c++ c++11 rvalue-reference c++-faq perfect-forwarding

c++ - Ventajas de usar adelante



c++11 rvalue-reference (6)

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

Si, después de crear una instancia, T1 es de tipo char , y T2 es de una clase, desea pasar t1 por copia y t2 por referencia const . Bueno, a menos que inner() tome por referencia no const , es decir, en cuyo caso también desea hacerlo.

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

Y luego 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 el puntero const haría una diferencia).

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

Ahora sabes por qué algunas mentes brillantes crearon un "reenvío perfecto": hace que el compilador haga todo esto por ti.

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

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


En el reenvío perfecto, std :: forward se utiliza para convertir la referencia de rvalue nombrada 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 interior 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 rvalor 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 outer como este

outer(17,29);

nos gustaría que 17 y 29 se reenvíen al # 2 porque 17 y 29 son literales enteros y como tales valores. Pero como t1 y t2 en la expresión inner(t1,t2); son valores, estarías invocando el # 1 en lugar del # 2. Es por eso que necesitamos convertir las referencias nuevamente en referencias sin nombre con std::forward . Por lo tanto, t1 en la parte outer siempre es una expresión de valor l, mientras que forward<T1>(t1) puede ser una expresión de valor en función de T1 . La última es solo una expresión de valor l, si T1 es una referencia de valor l. Y solo se deduce que T1 es una referencia de valor l en caso de que el primer argumento externo sea una expresión de valor l.


Creo que tener un código conceptual que implemente std :: forward puede agregarse a la discusión. Esta es una diapositiva de Scott Meyers, una muestra efectiva de C ++ 11/14.

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

Desde la perspectiva de un usuario, su significado es que std::forward es una conversión condicional a un valor de r. Puede ser útil si estoy escribiendo una función que espera un valor de l o valor en un parámetro y quiere pasarla a otra función como valor de r, solo si se pasó como valor de r. Si no envolvía 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, se 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 desde el inicio.


Puede valer la pena enfatizar que el reenvío debe usarse junto con un método externo con reenvío / referencia universal. El uso del reenvío por sí mismo como las siguientes declaraciones está permitido, pero no sirve más que para causar confusión. El comité estándar puede querer deshabilitar tal 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, avanzar y avanzar son patrones de diseño que son resultados naturales después de la introducción del tipo de referencia r-value. No debemos nombrar un método suponiendo que se usa correctamente a menos que esté prohibido el uso incorrecto.


Tienes que entender el problema de 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 fallan en algún sentido.

Lo más sencillo es usar una referencia-valor:

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

Pero esto no logra manejar los valores temporales: f(1, 2, 3); , ya que esos no pueden estar vinculados a una referencia-valor.

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 corrige el problema anterior, pero da la vuelta al flops. Ahora no permite que E tenga argumentos no constantes:

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 const-references, pero luego const_cast ''s the const away:

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 pasar 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. Proporcionas sobrecargas de f , con todas las combinaciones de const y non-const:

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);

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

(Esto es efectivamente lo que conseguimos 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 otro camino.

La solución es, en cambio, utilizar las referencias-valor añadido recientemente; podemos introducir nuevas reglas al deducir tipos de referencia de rvalor y crear cualquier resultado deseado. Después de todo, no podemos romper el código ahora.

Si se le da una referencia a una referencia (la referencia a una nota es un término que abarca tanto T& 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 reference to cv TR "crea el tipo" lvalue reference to T ", mientras que un intento de crear el tipo" rvalue reference to 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 lvalor A, suministramos el argumento de la plantilla con una referencia de lvalue a A. De lo contrario, deducimos normalmente. Esto da las llamadas referencias universales (el término referencia de reenvío es ahora el oficial).

¿Por qué es esto útil? Debido a que, 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 podría pasarse como un valor límico 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 tenemos! La solución es la siguiente:

static_cast<T&&>(x);

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

Cuando se nos ha pasado un valor, T es A , por lo que el tipo de destino para la conversión estática es A&& . La conversión genera una expresión de valor, que ya no se puede pasar a una referencia de valor l . 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 valor l, E obtiene un valor l. Cuando f recibe un valor, E obtiene un valor. Perfecto.

Y por supuesto, queremos deshacernos de lo feo. static_cast<T&&> es críptico y extraño 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);


Un punto que no se ha dejado muy claro es que static_cast<T&&> maneja const T& correctamente.
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.