Tienes que entender el problema de reenvío. Puede leer todo el problema 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 aspecto.
Lo más simple es usar 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 los valores temporales: f(1, 2, 3);
ya que no se pueden vincular a una referencia de 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 soluciona el problema anterior, pero voltea los flops. Ahora no permite E
tener 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 referencias constantes, pero luego const_cast
está 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. Proporciona sobrecargas de f
, con todas las 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 combinaciones de N , 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. Entonces tenemos que encontrar otra manera.
La solución es utilizar en su lugar las nuevas referencias de valor agregadas ; Podemos introducir nuevas reglas al deducir los tipos de referencia rvalue 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 de nota es un término abarcativo que significa ambos T&
y 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 de argumento de plantilla: si un argumento es un valor de l A, proporcionamos al argumento de plantilla una referencia de valor de l a A. De lo contrario, deducimos normalmente. Esto proporciona las llamadas referencias universales (el término referencia de reenvío ahora es oficial).
¿Por qué es útil esto? Debido a que combinados mantenemos la capacidad de realizar un seguimiento de la categoría de valor de un tipo: si era un valor de 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 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 esta:
static_cast<T&&>(x);
¿Qué hace esto? Considere que estamos dentro de la deduce
función y se nos ha pasado un valor. Esto significa que T
es a A&
, por lo que el tipo de destino para el reparto estático es A& &&
, o simplemente A&
. Como x
ya es un A&
, no hacemos nada y nos queda una referencia de valor.
Cuando se nos pasa un valor r, T
es A
, entonces el tipo de destino para el reparto estático es A&&
. La conversión produce 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 valor, E
obtiene un valor. 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; hagamos una función de utilidad llamada forward
, que hace lo mismo:
std::forward<A>(a);
// is the same as
static_cast<A&&>(a);
f
sería una función y no una expresión?const int i
será aceptado:A
se deduce aconst int
. Los fallos son para los literales de valores. 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
fuera un parámetro de valor). Simplemente loT
esint
. La razón que sex
evalúa como lvalue en el reenviador es porque las referencias rvalue denominadas se convierten en expresiones lvalue.forward
omove
aquí? ¿O es solo una diferencia semántica?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 de los dos. Úselostd::move
cuando sepa que ya no necesita el valor y desea moverlo a otro lugar,std::forward
hágalo de acuerdo con los valores pasados a su plantilla de función.Creo que tener un código conceptual que implemente std :: forward puede agregarse a la discusión. Esta es una diapositiva de la charla de Scott Meyers Un muestreador eficaz de C ++ 11/14
La función
move
en el código esstd::move
. Hay una implementación (funcional) para eso 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 de un usuario, su significado es que
std::forward
es una conversión condicional a un valor. Puede ser útil si estoy escribiendo una función que espera un valor 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 ajustara el parámetro en std :: forward, siempre se pasaría como una referencia normal.Efectivamente, imprime
El código se basa en un ejemplo de la charla mencionada anteriormente. Diapositiva 10, aproximadamente a las 15:00 desde el comienzo.
fuente
Si usa una referencia rvalue con nombre en una expresión, en realidad es un lvalue (porque se refiere al objeto por su nombre). Considere el siguiente ejemplo:
Ahora, si llamamos
outer
asínos gustaría que 17 y 29 se envíen al # 2 porque 17 y 29 son literales enteros y, como tales, valores. Pero dado que
t1
yt2
en la expresióninner(t1,t2);
son valores, estarías invocando el n. ° 1 en lugar del n. ° 2. Es por eso que necesitamos convertir las referencias de nuevo en referencias sin nombre constd::forward
. Entonces,t1
inouter
siempre es una expresión de valor mientras queforward<T1>(t1)
puede ser una expresión de valor dependiendo deT1
. Esta última es solo una expresión lvalue siT1
es una referencia lvalue. YT1
solo se deduce que es una referencia de valor en caso de que el primer argumento para exterior fuera una expresión de valor.fuente
Si, después de crear una instancia,
T1
es de tipochar
yT2
es de una clase, desea pasart1
por copia yt2
porconst
referencia. Bueno, a menos que losinner()
tome por noconst
referencia, es decir, en cuyo caso también desea hacerlo.Intente escribir un conjunto de
outer()
funciones que implementen esto sin referencias de valor, deduciendo la forma correcta de pasar los argumentos delinner()
tipo. Creo que necesitará algo 2 ^ 2 de ellos, bastante material de plantilla meta para deducir los argumentos, y mucho tiempo para hacerlo bien en todos los casos.Y luego alguien viene junto con un
inner()
que toma argumentos por puntero. Creo que ahora hace 3 ^ 2. (O 4 ^ 2. Demonios, no me molesto en tratar de pensar si elconst
puntero marcaría la diferencia).Y luego imagina que quieres hacer esto para cinco parámetros. O siete.
Ahora sabes por qué algunas mentes brillantes idearon el "reenvío perfecto": hace que el compilador haga todo esto por ti.
fuente
Un punto que no se ha aclarado es que también se
static_cast<T&&>
manejaconst T&
correctamente.Programa:
Produce:
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.
fuente
Puede valer la pena enfatizar que el reenvío debe usarse en conjunto con un método externo con reenvío / referencia universal. El uso de reenviar por sí mismo como las siguientes declaraciones está permitido, pero no sirve de nada más que causar confusión. El comité estándar puede desear deshabilitar dicha flexibilidad, de lo contrario, ¿por qué no usamos static_cast en su lugar?
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 se prohíba el uso incorrecto.
fuente