¿Cuáles son los propósitos principales de std::forward y qué problemas resuelve?
En reenvío perfecto, std::forward
se utiliza para convertir las referencias de rvalue con nombre 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 llamada inner
si dejamos t1
& t2
como valores l?
template <typename T1, typename T2>
void outer(T1&& t1, T2&& t2)
{
inner(std::forward<T1>(t1), std::forward<T2>(t2));
}
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 E
tener no const
argumentos:
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
se const
acabó:
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 deduce
función y se nos ha pasado un valor l. Esto significa T
que es a A&
, por lo que el tipo de objetivo para el lanzamiento estático es A& &&
, o simplemente A&
. Como x
ya es un A&
, no hacemos nada y nos queda una referencia de valor l.
Cuando se nos ha pasado un rvalue, T
es 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 f
recibe un valor l, E
obtiene un valor l. Cuando f
recibe un rvalue, E
obtiene 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);
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
La función move
en 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::forward
es 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.
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 outer
así
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 t1
y t2
en 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, t1
in outer
es 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 T1
es una referencia de valor l. Y T1
solo 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.
¿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, T1
es de tipo char
y T2
es de una clase, desea pasar t1
por copia y t2
por const
referencia. Bueno, a menos que inner()
los tome por no const
referencia, 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 const
el 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.