c++ - ¿Puedo normalmente/siempre usar std:: forward en lugar de std:: move?
c++11 move-semantics (2)
He estado viendo la charla de Scott Meyers sobre referencias universales de la conferencia C ++ y Beyond 2012, y todo tiene sentido hasta ahora. Sin embargo, un miembro de la audiencia hace una pregunta de unos 50 minutos en la que también me preguntaba. Meyers dice que no le importa la respuesta porque no es idiomática y sería tonto, pero aún estoy interesado.
El código presentado es el siguiente:
// Typical function bodies with overloading:
void doWork(const Widget& param) // copy
{
// ops and exprs using param
}
void doWork(Widget&& param) // move
{
// ops and exprs using std::move(param)
}
// Typical function implementations with universal reference:
template <typename T>
void doWork(T&& param) // forward => copy and move
{
// ops and exprs using std::forward<T>(param)
}
El punto es que cuando tomamos una referencia de valor real, sabemos que tenemos un valor r, así que debemos std::move
para preservar el hecho de que es un valor r. Cuando tomamos una referencia universal ( T&&
, donde T
es un tipo deducido), queremos que std::forward
preserve el hecho de que puede haber sido un valor l o un valor r.
Entonces la pregunta es: dado que std::forward
preserva si el valor pasado a la función era un valor lval o un valor r, y std::move
simplemente lanza su argumento a un valor r, ¿podríamos simplemente usar std::forward
cualquier lugar? std::forward
comportaría como std::move
en todos los casos en que std::move
, o hay algunas diferencias importantes en el comportamiento que se pierden por la generalización de Meyers?
No estoy sugiriendo que alguien deba hacerlo porque, como Meyers dice correctamente, es completamente no idiomático, pero es el siguiente también un uso válido de std::move
:
void doWork(Widget&& param) // move
{
// ops and exprs using std::forward<Widget>(param)
}
Las dos son herramientas muy diferentes y complementarias .
std::move
deduce el argumento y crea incondicionalmente una expresión rvalue. Esto tiene sentido para aplicar a un objeto o variable real.std::forward
toma un argumento de plantilla obligatorio (¡debes especificar esto!) y mágicamente crea una referencia lvalue o una expresión rvalue dependiendo de qué tipo fue (en virtud de agregar&&
y las reglas colapsadas). Esto solo tiene sentido para aplicar a un argumento de función deducido y con plantilla.
Quizás los siguientes ejemplos ilustran esto un poco mejor:
#include <utility>
#include <memory>
#include <vector>
#include "foo.hpp"
std::vector<std::unique_ptr<Foo>> v;
template <typename T, typename ...Args>
std::unique_ptr<T> make_unique(Args &&... args)
{
return std::unique_ptr<T>(new T(std::forward<Args>(args)...)); // #1
}
int main()
{
{
std::unique_ptr<Foo> p(new Foo(''a'', true, Bar(1,2,3)));
v.push_back(std::move(p)); // #2
}
{
v.push_back(make_unique<Foo>(''b'', false, Bar(5,6,7))); // #3
}
{
Bar b(4,5,6);
char c = ''x'';
v.push_back(make_unique<Foo>(c, b.ready(), b)); // #4
}
}
En la situación n. ° 2, tenemos un objeto concreto existente p
, y queremos pasar de él, incondicionalmente. Solo std::move
tiene sentido. No hay nada que "reenviar" aquí. Tenemos una variable con nombre y queremos pasar de ella.
Por otro lado, la situación n. ° 1 acepta una lista de cualquier tipo de argumentos, y cada argumento debe reenviarse como la misma categoría de valor que en la llamada original. Por ejemplo, en el n. ° 3, los argumentos son expresiones temporales y, por lo tanto, se enviarán como valores r. Pero también podríamos haber mezclado objetos nombrados en la llamada de constructor, como en la situación n. ° 4, y luego necesitamos reenviar como valores l.
Sí, si param
es un Widget&&
, entonces las siguientes tres expresiones son equivalentes (suponiendo que Widget
no sea un tipo de referencia):
std::move(param)
std::forward<Widget>(param)
static_cast<Widget&&>(param)
En general (cuando Widget
puede ser una referencia), std::move(param)
es equivalente a las dos expresiones siguientes:
std::forward<std::remove_reference<Widget>::type>(param)
static_cast<std::remove_reference<Widget>::type&&>(param)
Tenga en cuenta cuánto más std::move
es más agradable para mover cosas. El objetivo de std::forward
es que se mezcle bien con las reglas de deducción del tipo de plantilla:
template<typename T>
void foo(T&& t) {
std::forward<T>(t);
std::move(t);
}
int main() {
int a{};
int const b{};
//Deduced T Signature Result of `forward<T>` Result of `move`
foo(a); //int& foo(int&) lvalue int xvalue int
foo(b); //int const& foo(int const&) lvalue int const xvalue int const
foo(int{});//int foo(int&&) xvalue int xvalue int
}