¿Cuál es la diferencia entre std :: move y std :: forward

160

Vi esto aquí: Move Constructor llamando a clase base Move Constructor

Podría alguien explicar:

  1. la diferencia entre std::movey std::forward, preferiblemente con algunos ejemplos de código?
  2. Cómo pensarlo fácilmente y cuándo usar qué
aCuria
fuente
1
Consulte también estas dos preguntas relacionadas: ¿Debería usar std :: move o std :: forward en los operadores de movimiento / asignación? y ¿Cómo funciona std :: forward? (así como la pregunta duplicada).
Xeo
"Cómo pensarlo fácilmente y cuándo usar qué" Usas movecuando quieres mover un valor y forwardcuando quieres usar el reenvío perfecto. Esto no es ciencia espacial aquí;)
Nicol Bolas
move () realiza un lanzamiento incondicional mientras que como forward () realiza un lanzamiento basado en el parámetro que ha pasado.
RaGa__M

Respuestas:

159

std::movetoma un objeto y le permite tratarlo como temporal (un valor r). Aunque no es un requisito semántico, normalmente una función que acepta una referencia a un valor r lo invalidará. Cuando vea std::move, indica que el valor del objeto no debe usarse después, pero aún puede asignar un nuevo valor y continuar usándolo.

std::forwardtiene un solo caso de uso: para convertir un parámetro de función con plantilla (dentro de la función) a la categoría de valor (lvalue o rvalue) que la persona que llama usó para pasarlo. Esto permite que los argumentos de valor se transmitan como valores, y los valores se transmitan como valores, un esquema llamado "reenvío perfecto".

Para ilustrar :

void overloaded( int const &arg ) { std::cout << "by lvalue\n"; }
void overloaded( int && arg ) { std::cout << "by rvalue\n"; }

template< typename t >
/* "t &&" with "t" being template param is special, and  adjusts "t" to be
   (for example) "int &" or non-ref "int" so std::forward knows what to do. */
void forwarding( t && arg ) {
    std::cout << "via std::forward: ";
    overloaded( std::forward< t >( arg ) );
    std::cout << "via std::move: ";
    overloaded( std::move( arg ) ); // conceptually this would invalidate arg
    std::cout << "by simple passing: ";
    overloaded( arg );
}

int main() {
    std::cout << "initial caller passes rvalue:\n";
    forwarding( 5 );
    std::cout << "initial caller passes lvalue:\n";
    int x = 5;
    forwarding( x );
}

Como Howard menciona, también hay similitudes ya que ambas funciones simplemente se convierten al tipo de referencia. Pero fuera de estos casos de uso específicos (que cubren el 99.9% de la utilidad de los moldes de referencia de valor), debe usar static_castdirectamente y escribir una buena explicación de lo que está haciendo.

Agua de patata
fuente
No estoy seguro de que sea exacto que std::forwardel único caso de uso sea el reenvío perfecto de argumentos de función. Me he encontrado con situaciones en las que quiero reenviar perfectamente otras cosas, como miembros del objeto.
Geoff Romer
@GeoffRomer ¿Miembros de un parámetro? ¿Tienes un ejemplo?
Potatoswatter
Publiqué un ejemplo como una pregunta separada: stackoverflow.com/questions/20616958/…
Geoff Romer
Hmm, así que si entiendo esto correctamente, significa que puedo escribir una sola función forward () donde forward (5) opera en 5 como si lo pasara por valor donde forward (x) opera en x como si lo pasara por referencia sin tener que escribir explícitamente una sobrecarga.
iheanyi
@iheanyi Sí, esa es la idea! Pero tiene que ser una plantilla, no solo una función ordinaria.
Potatoswatter
63

Ambos std::forwardy std::moveno son más que moldes.

X x;
std::move(x);

Lo anterior xconvierte la expresión lvalue de tipo X en una expresión rvalue de tipo X (un valor x para ser exactos). moveTambién puede aceptar un valor r:

std::move(make_X());

y en este caso es una función de identidad: toma un valor de tipo X y devuelve un valor de tipo X.

Con std::forwardusted puede seleccionar el destino hasta cierto punto:

X x;
std::forward<Y>(x);

Convierte la expresión lvalue xde tipo X en una expresión de tipo Y. Existen restricciones sobre lo que Y puede ser.

Y puede ser una Base de X accesible, o una referencia a una Base de X. Y puede ser X, o una referencia a X. No se pueden descartar los calificadores cv forward, pero se pueden agregar calificadores cv. Y no puede ser un tipo que sea simplemente convertible de X, excepto a través de una conversión de Base accesible.

Si Y es una referencia de valor, el resultado será una expresión de valor. Si Y no es una referencia de lvalue, el resultado será una expresión de rvalue (xvalue para ser precisos).

forwardpuede tomar un argumento rvalue solo si Y no es una referencia lvalue. Es decir, no puede emitir un valor r a lvalue. Esto se debe a razones de seguridad, ya que esto generalmente lleva a referencias colgantes. Pero emitir un rvalue a rvalue está bien y está permitido.

Si intenta especificar Y a algo que no está permitido, el error se detectará en tiempo de compilación, no en tiempo de ejecución.

Howard Hinnant
fuente
Si reenvío perfectamente un objeto a una función std::forward, luego de ejecutar esa función, ¿puedo usar ese objeto? Soy consciente de que, en caso de std::move, es un comportamiento indefinido.
iammilind
1
Con respecto a move: stackoverflow.com/a/7028318/576911 Para forward, si pasa un valor, su API debería reaccionar como si recibiera un valor. Normalmente esto significa que el valor no se modificará. Pero si es un valor no constante, su API puede haberlo modificado. Si pasa un valor r, esto normalmente significa que su API puede haberse movido de él y, por lo tanto, se aplicaría stackoverflow.com/a/7028318/576911 .
Howard Hinnant
21

std::forwardse usa para reenviar un parámetro exactamente de la forma en que se pasó a una función. Justo como se muestra aquí:

¿Cuándo usar std :: forward para reenviar argumentos?

El uso std::moveofrece un objeto como un valor r, para posiblemente hacer coincidir un constructor de movimiento o una función que acepte valores. Lo hace std::move(x)incluso si xno es un valor por sí mismo.

Bo Persson
fuente