c++ - ¿Hay casos de uso para std:: forward con un prvalue?
c++11 move-semantics (3)
El uso más común de std::forward
es, bueno, perfeccionar una referencia de reenvío (universal), como
template<typename T>
void f(T&& param)
{
g(std::forward<T>(param)); // perfect forward to g
}
Aquí param
es un lvalue
, y std::forward
termina convirtiéndolo en un rvalue o lvalue, dependiendo de lo que fue el argumento que lo limitaba.
Mirando la definición de std::forward
de cppreference.com veo que también hay una sobrecarga de rvalue
template< class T >
T&& forward( typename std::remove_reference<T>::type&& t );
¿Alguien puede darme alguna razón por la sobrecarga de rvalue
? No puedo ver ningún caso de uso. Si desea pasar un rvalor a una función, simplemente puede pasarlo como está, sin necesidad de aplicar std::forward
en él.
Esto es diferente de std::move
, donde veo por qué uno quiere también una sobrecarga de rvalue
: puede tratar con código genérico en el que no sabe qué se está pasando y quiere soporte incondicional para mover semántica, vea por ejemplo ¿Por qué std :: move toma una referencia universal? .
EDITAR Para aclarar la pregunta, estoy preguntando por qué es necesaria la sobrecarga (2) desde aquí , y un caso de uso para ello.
Esta respuesta es para responder un comentario por @vsoftco.
@DarioOO gracias por el enlace. ¿Puedes tal vez escribir una respuesta sucinta? A partir de su ejemplo, todavía no está claro por qué std :: forward también debe definirse para rvalues
En breve:
Porque sin una especialización de valor no se compilaría el siguiente código
#include <utility>
#include <vector>
using namespace std;
class Library
{
vector<int> b;
public:
// hi! only rvalue here :)
Library( vector<int>&& a):b(std::move(a)){
}
};
int main()
{
vector<int> v;
v.push_back(1);
A a( forward<vector<int>>(v));
return 0;
}
Sin embargo, no puedo resistirme a escribir más, así que aquí también está la versión no sucinta de la respuesta.
Versión larga:
Debe mover v
porque la Library
clases no tiene un constructor que acepte lvalue, sino solo una referencia rvalue. Sin un reenvío perfecto, terminaríamos en un comportamiento no deseado:
Las funciones de envoltura incurrirían en una penalidad de alto rendimiento al pasar objetos pesados.
con la semántica de movimiento nos aseguramos de que se use el constructor de movimiento SI ES POSIBLE. En el ejemplo anterior, si eliminamos std::forward
el código no se compilará.
Entonces, ¿qué está haciendo realmente hacia forward
? ¿Moviendo el elemento sin nuestro consenso? No
Es solo crear una copia del vector y moverla. ¿Cómo podemos estar seguros de eso? Simplemente intenta acceder al elemento.
vector<int> v;
v.push_back(1);
A a( forward<vector<int>>(v)); //what happens here? make a copy and move
std::cout<<v[0]; // OK! std::forward just "adapted" our vector
si en cambio mueves ese elemento
vector<int> v;
v.push_back(1);
A a( std::move(v)); //what happens here? just moved
std::cout<<v[0]; // OUCH! out of bounds exception
De modo que se necesita una sobrecarga para hacer posible una conversión implícita que aún es segura, pero no es posible sin la sobrecarga.
De hecho, el siguiente código simplemente no se compilará:
vector<int> v;
v.push_back(1);
A a( v); //try to copy, but not find a lvalue constructor
Caso de uso real:
Puede argumentar que los argumentos de reenvío pueden crear copias inútiles y, por lo tanto, ocultar un posible impacto de rendimiento, sí, eso es realmente cierto, pero considere casos de uso reales:
template< typename Impl, typename... SmartPointers>
static std::shared_ptr<void>
instancesFactoryFunction( priv::Context * ctx){
return std::static_pointer_cast<void>( std::make_shared<Impl>(
std::forward< typename SmartPointers::pointerType>(
SmartPointers::resolve(ctx))...
) );
}
El código fue tomado de mi marco (línea 80): Infectorpp 2
En ese caso, los argumentos se reenvían desde una llamada de función. SmartPointers::resolve
valores devueltos de SmartPointers::resolve
se mueven correctamente, independientemente del hecho de que el constructor de Impl acepte rvalue o lvalue (por lo que no hay errores de compilación y esos se mueven de todos modos).
Básicamente, puedes usar std::foward
en cualquier caso en el que quieras hacer el código más simple y más legible, pero debes tener en cuenta 2 puntos
- Tiempo extra de compilación (no tanto en la realidad)
- puede causar copias no deseadas (cuando no mueve explícitamente algo a algo que requiere un valor de valor)
Si se usa con cuidado es una herramienta poderosa.
Me fijé en esta pregunta antes, leí el enlace de Howard Hinnant, no pude asimilarlo completamente después de una hora de pensar. Ahora estaba buscando y obtuve la respuesta en cinco minutos. (Editar: obtuve la respuesta es demasiado generosa, ya que el enlace de Hinnant tenía la respuesta. Quise decir que entendí y pude explicarlo de una manera más simple, que espero que alguien encuentre útil).
Básicamente, esto le permite ser genérico en ciertos tipos de situaciones dependiendo del tipo que se haya pasado. Considere este código:
#include <utility>
#include <vector>
#include <iostream>
using namespace std;
class GoodBye
{
double b;
public:
GoodBye( double&& a):b(std::move(a)){ std::cerr << "move"; }
GoodBye( const double& a):b(a){ std::cerr << "copy"; }
};
struct Hello {
double m_x;
double & get() { return m_x; }
};
int main()
{
Hello h;
GoodBye a(std::forward<double>(std::move(h).get()));
return 0;
}
Este código imprime "mover". Lo interesante es que si quito el std::forward
, se imprime copia. Esto, para mí, es difícil de comprender, pero aceptémoslo y sigamos adelante. (Edición: supongo que esto sucede porque get devolverá una referencia de valor l a un valor de r. Una entidad de este tipo se desintegra en un valor de l, pero std :: forward lo convertirá en un valor de r, al igual que en el uso común de forward. Todavía se siente poco intuitivo aunque).
Ahora, imaginemos otra clase:
struct Hello2 {
double m_x;
double & get() & { return m_x; }
double && get() && { return std::move(m_x); }
};
Supongamos que en el código main
, h
era una instancia de Hello2. Ahora, ya no necesitamos std :: forward, porque la llamada a std::move(h).get()
devuelve un valor de r. Sin embargo, supongamos que el código es genérico:
template <class T>
void func(T && h) {
GoodBye a(std::forward<double>(std::forward<T>(h).get()));
}
Ahora, cuando llamamos func
, nos gustaría que funcione correctamente tanto con Hello
como con Hello2
, es decir, nos gustaría activar un movimiento. Eso solo sucede con un valor de Hello
si incluimos la estándar std::forward
, por lo que lo necesitamos. Pero ... Llegamos al remate. Cuando pasamos un valor de Hello2
a esta función, la sobrecarga de valor de get () ya devolverá un valor doble, de modo que std::forward
está aceptando un valor real. Por lo tanto, si no fuera así, no podría escribir código totalmente genérico como se indicó anteriormente.
Maldita sea.
Ok, ya que @vsoftco solicitó un caso de uso conciso, aquí hay una versión refinada (usando su idea de tener "my_forward" para ver cómo se llama a la sobrecarga).
Interpreto "caso de uso" al proporcionar un ejemplo de código que, sin prvalue, no se compila ni se comporta de manera diferente (independientemente de que sea realmente útil o no).
Tenemos 2 sobrecargas para std::forward
#include <iostream>
template <class T>
inline T&& my_forward(typename std::remove_reference<T>::type& t) noexcept
{
std::cout<<"overload 1"<<std::endl;
return static_cast<T&&>(t);
}
template <class T>
inline T&& my_forward(typename std::remove_reference<T>::type&& t) noexcept
{
std::cout<<"overload 2"<<std::endl;
static_assert(!std::is_lvalue_reference<T>::value,
"Can not forward an rvalue as an lvalue.");
return static_cast<T&&>(t);
}
Y tenemos 4 posibles casos de uso.
Caso de uso 1
#include <vector>
using namespace std;
class Library
{
vector<int> b;
public:
// &&
Library( vector<int>&& a):b(std::move(a)){
}
};
int main()
{
vector<int> v;
v.push_back(1);
Library a( my_forward<vector<int>>(v)); // &
return 0;
}
Caso de uso 2
#include <vector>
using namespace std;
class Library
{
vector<int> b;
public:
// &&
Library( vector<int>&& a):b(std::move(a)){
}
};
int main()
{
vector<int> v;
v.push_back(1);
Library a( my_forward<vector<int>>(std::move(v))); //&&
return 0;
}
Caso de uso 3
#include <vector>
using namespace std;
class Library
{
vector<int> b;
public:
// &
Library( vector<int> a):b(a){
}
};
int main()
{
vector<int> v;
v.push_back(1);
Library a( my_forward<vector<int>>(v)); // &
return 0;
}
Caso de uso 4
#include <vector>
using namespace std;
class Library
{
vector<int> b;
public:
// &
Library( vector<int> a):b(a){
}
};
int main()
{
vector<int> v;
v.push_back(1);
Library a( my_forward<vector<int>>(std::move(v))); //&&
return 0;
}
Aquí hay un currículum
- Se usa la sobrecarga 1, sin ella se obtiene un error de compilación
- Se utiliza la sobrecarga 2, sin ella se obtiene un error de compilación
- Se utiliza la sobrecarga 1, sin ello se obtiene un error de compilación
- Se utiliza la sobrecarga 2, sin ella se obtiene un error de compilación
Tenga en cuenta que si no utilizamos adelante.
Library a( std::move(v));
//and
Library a( v);
usted obtiene:
- Error de compilación
- Compilar
- Compilar
- Compilar
Como puede ver, si usa solo una de las dos sobrecargas forward
, básicamente causa que no compile 2 de los 4 casos, mientras que si no usa la forward
en absoluto, solo compilará 3 de los 4 casos.