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?