2010-08-27 15 views
324

En el reenvío perfecto, std::forward se utiliza para convertir las referencias rvalue con nombre t1 y t2 en 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 dejamos t1 & t2 como lvalues?Ventajas del uso de

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

Respuesta

11

¿Cómo afectaría la función llamada interior si dejamos t1 t2 & como valor-I?

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

Trate de escribir un conjunto de funciones outer() que implementen esto sin referencias rvalue, deduciendo la forma correcta de pasar los argumentos del tipo inner(). Creo que necesitarás algo de 2^2 de ellos, una plantilla de plantilla bastante pesada para deducir los argumentos, y mucho tiempo para hacerlo bien 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 me puedo molestar en tratar de pensar si el puntero const marcaría la diferencia.)

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

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

607

Tienes que entender el problema de reenvío. Puede read the entire problem in detail, 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 aspecto.


Lo más sencillo es utilizar un valor-i-referencia:

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

Pero esto no puede manejar los valores temporales: f(1, 2, 3);, como los que no se puede enlazar a un valor-i-referencia.

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

que fija el problema anterior, pero que pasa flops.Ahora deja de permitir E que tienen argumentos no const:

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 referencias, pero luego const_cast 's la const distancia:

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

Este 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 se encarga de todo correctamente ... a costa de estar imposible de mantener Usted proporciona sobrecargas de f, con todos combinaciones de const y no 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); 

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

(Esto es efectivamente lo que obtenemos el compilador para hacer por nosotros en C++ 11.)


En C++ 11, tenemos la oportunidad de solucionar este problema. One solution modifies template deduction rules on existing types, but this potentially breaks a great deal of code. Así que tenemos que encontrar otra manera.

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

Si se les da una referencia a una referencia (referencia de la nota es un término que significa que abarca tanto T& y T&&), se utiliza la siguiente regla para averiguar el tipo resultante:

"[da] un tipo TR eso es una referencia a un tipo T, un intento de crear el tipo "referencia de lvalue a cv TR" crea el tipo "referencia de lvalue a T", mientras que un intento de crear el tipo "referencia de rvalue 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 deducción de argumento de plantilla: si un argumento es un valor-A, que les suministra el argumento de plantilla con una referencia a lvalue A. De lo contrario, se deduce normalmente . Esto proporciona las llamadas referencias universales (el término forwarding reference es ahora el oficial).

¿Por qué es esto útil? Debido a que combinamos, mantenemos la capacidad de realizar un seguimiento de la categoría de valor de un tipo: si se trata de un valor l, tenemos un parámetro de referencia lvalue; de ​​lo contrario, tenemos un parámetro de referencia rvalue.

En código:

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 a la categoría de valor de la variable "hacia adelante".Tenga en cuenta, una vez dentro de la función del parámetro se podría pasar como un valor izquierdo 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 deduce, y hemos recibido un lvalue. Esto significa que T es A&, por lo que el tipo de objetivo para el molde estático es A& &&, o simplemente A&. Dado que x ya es un A&, no hacemos nada y nos queda una referencia lvalue.

Cuando se nos ha pasado un valor r, T es A, por lo que el tipo de destino para el modelo estático es A&&. El elenco da como resultado una expresión rvalue, , que ya no se puede pasar a una referencia de lvalue. Hemos mantenido la categoría de valor del parámetro.

Poner esto junto nos da "reenvío perfecto":

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

Cuando f recibe un valor izquierdo, E obtiene un valor izquierdo. 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 extraño de recordar; En lugar de eso crea una función de utilidad llamada forward, que hace lo mismo:

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

¿No sería 'f' una función, y no una expresión? –

+0

@sbi: Haha. :) <3 @mfukar: Es una expresión de llamada a función. 'f' en sí mismo podría ser cualquier cosa" invocable "; una función, puntero de función u objeto de función. – GManNickG

+0

Gracias por su respuesta integral. Solo una pregunta, en deducir (1), x es tipo int && o int? Porque creo que leí en alguna parte que, si es de tipo rvalue, la T se resolvería en int. Entonces, con el extra &&, se convertiría en int && type. ¿Alguien podría aclarar eso? http://thbecker.net/articles/rvalue_references/section_08.html – Steveng

13

En perfecta expedición, std :: hacia delante se utiliza para convertir la referencia t1 y t2 rvalue llamado a la referencia rvalue sin nombre. ¿Cuál es el propósito de hacer eso? ¿Cómo afectaría eso a la función llamada inner si dejamos t1 & t2 como lvalue?

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

Si utiliza una referencia rvalue nombrado en una expresión que es en realidad un valor izquierdo (debido a que se refieren al objeto por su nombre). Consideremos el siguiente ejemplo:

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

Ahora, si llamamos outer como esto

outer(17,29); 

nos gustaría 17 y 29 que se remitirá a # 2, ya que el 17 y 29 son literales enteros y, como tal rvalues . Pero desde t1 y t2 en la expresión inner(t1,t2); son lvalues, estarías invocando # 1 en lugar de # 2. Es por eso que debemos volver las referencias a referencias sin nombre con std::forward. Por lo tanto, t1 en outer es siempre una expresión lvalue, mientras que forward<T1>(t1) puede ser una expresión rvalue según T1. Esta última es solo una expresión lvalue si T1 es una referencia lvalue. Y T1 solo se deduce como una referencia de lvalue en caso de que el primer argumento de outer fuera una expresión lvalue.

+0

Finalmente, explicación clara :) –

29

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

conceptual code implementing std::forward

Función move en el código es std::move. Hay una implementación (de trabajo) para él antes en esa charla. Encontré actual implementation of std::forward in libstdc++, en el archivo move.h, pero no es del todo instructivo.

Desde la perspectiva de los usuarios, 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 lvalue o r en un parámetro y desea pasarlo a otra función como un valor r solo si se pasó como un valor r. Si no envolví el parámetro en std :: forward, siempre pasará 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 

Código se basa en un ejemplo de la charla se ha mencionado anteriormente. Diapositiva 10, aproximadamente a las 15:00 desde el comienzo.

2

Un punto que no se ha hecho claro como el cristal es que static_cast<T&&> se encarga de 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 se define como 'void f (int & & a)' esto no funciona.

+0

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

1

Puede valer la pena enfatizar que forward debe usarse en tándem con un método externo con reenvío/referencia universal. El uso de reenviar por sí mismo como las siguientes afirmaciones está permitido, pero no sirve para nada más que causar confusión. El comité estándar puede querer desactivar dicha 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, se mueven hacia adelante y son los patrones de diseño que son resultados naturales después de que se introdujo el tipo de referencia r-valor. No deberíamos nombrar un método suponiendo que se usa correctamente a menos que se prohíba el uso incorrecto.

+0

No creo que el comité C++ sienta que le corresponde a ellos utilizar las expresiones idiomáticas "correctamente", ni siquiera definir qué uso "correcto" es (aunque ciertamente pueden dar pautas). Con ese fin, aunque los profesores, jefes y amigos de una persona pueden tener el deber de dirigirlos de una forma u otra, creo que el comité de C++ (y por lo tanto el estándar) no tiene ese deber. – SirGuy

+0

Sí, acabo de leer [N2951] (http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2009/n2951.html) y acepto que el comité estándar no tiene la obligación de agregar limitaciones innecesarias con respecto al uso de una función. Pero los nombres de estas dos plantillas de funciones (mover y reenviar) son un poco confusas, ya que solo ven sus definiciones en el archivo de la biblioteca o en la documentación estándar (23.2.5 Reenviar/mover ayudantes). Los ejemplos en el estándar definitivamente ayudan a entender el concepto, pero podría ser útil agregar más comentarios para aclarar un poco más las cosas. – colin

Cuestiones relacionadas