¿Cómo transfiere std :: move () valores a RValues?

104

Me encontré sin comprender completamente la lógica de std::move() .

Al principio, lo busqué en Google, pero parece que solo hay documentos sobre cómo usar std::move() , no cómo funciona su estructura.

Quiero decir, sé cuál es la función del miembro de la plantilla, pero cuando miro std::move() definición en VS2010, todavía es confuso.

la definición de std :: move () va más abajo.

template<class _Ty> inline
typename tr1::_Remove_reference<_Ty>::_Type&&
    move(_Ty&& _Arg)
    {   // forward _Arg as movable
        return ((typename tr1::_Remove_reference<_Ty>::_Type&&)_Arg);
    }

Lo que es extraño primero para mí es el parámetro, (_Ty && _Arg), porque cuando llamo a la función como ves a continuación,

// main()
Object obj1;
Object obj2 = std::move(obj1);

Básicamente equivale a

// std::move()
_Ty&& _Arg = Obj1;

Pero como ya sabes, no puedes vincular directamente un LValue a una referencia de RValue, lo que me hace pensar que debería ser así.

_Ty&& _Arg = (Object&&)obj1;

Sin embargo, esto es absurdo porque std :: move () debe funcionar para todos los valores.

Así que supongo que para entender completamente cómo funciona esto, debería echar un vistazo a estas estructuras también.

template<class _Ty>
struct _Remove_reference
{   // remove reference
    typedef _Ty _Type;
};

template<class _Ty>
struct _Remove_reference<_Ty&>
{   // remove reference
    typedef _Ty _Type;
};

template<class _Ty>
struct _Remove_reference<_Ty&&>
{   // remove rvalue reference
    typedef _Ty _Type;
};

Desafortunadamente, sigue siendo tan confuso y no lo entiendo.

Sé que todo esto se debe a mi falta de conocimientos básicos de sintaxis sobre C ++. Me gustaría saber cómo funcionan a fondo y cualquier documento que pueda obtener en Internet será más que bienvenido. (Si puedes explicar esto, también será genial).

Dean Seo
fuente
31
@NicolBolas Por el contrario, la pregunta en sí misma muestra que OP se está subestimando a sí mismo en esa declaración. Es precisamente la comprensión de OP de las habilidades de sintaxis básica lo que les permite incluso plantear la pregunta.
Kyle Strand
De todos modos, no estoy de acuerdo: puedes aprender rápidamente o ya tienes un buen conocimiento de la informática o la programación en su conjunto, incluso en C ++, y todavía tienes problemas con la sintaxis. Es mejor dedicar su tiempo a aprender mecánicas, algoritmos, etc. pero confiar en las referencias para repasar la sintaxis según sea necesario, que ser técnicamente articulado pero tener una comprensión superficial de cosas como copiar / mover / avanzar. ¿Cómo se supone que debe saber "cuándo y cómo usar std :: move cuando desea mover construir algo" antes de saber qué hace el movimiento?
John P
Creo que vine aquí para entender cómo movefunciona en lugar de cómo se implementa. Encuentro esta explicación realmente útil: pagefault.blog/2018/03/01/… .
Anton Daneyko

Respuestas:

171

Comenzamos con la función de movimiento (que limpié un poco):

template <typename T>
typename remove_reference<T>::type&& move(T&& arg)
{
  return static_cast<typename remove_reference<T>::type&&>(arg);
}

Comencemos con la parte más fácil, es decir, cuando se llama a la función con rvalue:

Object a = std::move(Object());
// Object() is temporary, which is prvalue

y nuestra moveplantilla se crea una instancia de la siguiente manera:

// move with [T = Object]:
remove_reference<Object>::type&& move(Object&& arg)
{
  return static_cast<remove_reference<Object>::type&&>(arg);
}

Dado que se remove_referenceconvierte T&en To T&&en T, y Objectno es una referencia, nuestra función final es:

Object&& move(Object&& arg)
{
  return static_cast<Object&&>(arg);
}

Ahora, podría preguntarse: ¿necesitamos siquiera el yeso? La respuesta es: sí, lo hacemos. La razón es simple; la referencia rvalue nombrada se trata como lvalue (y la conversión implícita de la referencia lvalue a rvalue está prohibida por estándar).


Esto es lo que sucede cuando llamamos movecon lvalue:

Object a; // a is lvalue
Object b = std::move(a);

y la correspondiente moveinstanciación:

// move with [T = Object&]
remove_reference<Object&>::type&& move(Object& && arg)
{
  return static_cast<remove_reference<Object&>::type&&>(arg);
}

Una vez más, remove_referenceconvertidos Object&a Objecty obtenemos:

Object&& move(Object& && arg)
{
  return static_cast<Object&&>(arg);
}

Ahora llegamos a la parte complicada: ¿qué significa Object& &&incluso y cómo se puede vincular a lvalue?

Para permitir un reenvío perfecto, el estándar C ++ 11 proporciona reglas especiales para el colapso de referencias, que son las siguientes:

Object &  &  = Object &
Object &  && = Object &
Object && &  = Object &
Object && && = Object &&

Como puede ver, bajo estas reglas Object& &&realmente significa Object&, que es una referencia lvalue simple que permite vincular lvalues.

La función final es así:

Object&& move(Object& arg)
{
  return static_cast<Object&&>(arg);
}

que no es diferente de la instanciación anterior con rvalue: ambos lanzan su argumento a la referencia rvalue y luego lo devuelven. La diferencia es que la primera instanciación se puede usar solo con rvalues, mientras que la segunda funciona con lvalues.


Para explicar por qué necesitamos remove_referenceun poco más, probemos esta función

template <typename T>
T&& wanna_be_move(T&& arg)
{
  return static_cast<T&&>(arg);
}

y instanciarlo con lvalue.

// wanna_be_move [with T = Object&]
Object& && wanna_be_move(Object& && arg)
{
  return static_cast<Object& &&>(arg);
}

Aplicando las reglas de colapso de referencia mencionadas anteriormente, puede ver que obtenemos una función que no se puede usar como move(para decirlo de manera simple, la llama con lvalue, obtiene lvalue de vuelta). En todo caso, esta función es la función de identidad.

Object& wanna_be_move(Object& arg)
{
  return static_cast<Object&>(arg);
}
Vitus
fuente
3
Buena respuesta. Aunque entiendo que para un lvalue es una buena idea evaluar Tcomo Object&, no sabía que esto estaba realmente hecho. Hubiera esperado Tevaluar también Objecten este caso, ya que pensé que esta era la razón para introducir referencias de contenedor y std::ref, o no lo era.
Christian Rau
2
Hay una diferencia entre template <typename T> void f(T arg)(que es lo que está en el artículo de Wikipedia) y template <typename T> void f(T& arg). El primero se resuelve en valor (y si desea pasar la referencia, debe ajustarlo std::ref), mientras que el segundo siempre se resuelve en referencia. Lamentablemente, las reglas para la deducción de argumentos de plantilla son bastante complejas, por lo que no puedo proporcionar un razonamiento preciso por qué se T&&resuelve Object& &&(pero sucede de hecho).
Vitus
1
Pero, ¿hay alguna razón por la que este enfoque directo no funcione? template <typename T> T&& also_wanna_be_move(T& arg) { return static_cast<T&&>(arg); }
Greggo
1
Eso explica por qué es necesario remove_reference, pero todavía no entiendo por qué la función tiene que tomar T && (en lugar de T &). Si entiendo esta explicación correctamente, no debería importar si obtienes un T && o T & porque de cualquier manera, remove_reference lo convertirá en T, luego volverás a agregar el &&. Entonces, ¿por qué no decir que acepta un T & (que semánticamente es lo que está aceptando), en lugar de T && y confiar en el reenvío perfecto para permitir que la persona que llama pase un T &?
mgiuca
3
@mgiuca: Si std::movesolo desea convertir lvalues ​​a rvalues, entonces sí, T&estaría bien. Este truco se hace principalmente por flexibilidad: puede llamar std::movea todo (rvalues ​​incluidos) y recuperar rvalue.
Vitus
4

_Ty es un parámetro de plantilla, y en esta situación

Object obj1;
Object obj2 = std::move(obj1);

_Ty es del tipo "Objeto &"

por eso es necesario _Remove_reference.

Seria mas como

typedef Object& ObjectRef;
Object obj1;
ObjectRef&& obj1_ref = obj1;
Object&& obj2 = (Object&&)obj1_ref;

Si no eliminamos la referencia, sería como lo estábamos haciendo

Object&& obj2 = (ObjectRef&&)obj1_ref;

Pero ObjectRef && se reduce a Object &, que no pudimos vincular a obj2.

La razón por la que se reduce de esta manera es para admitir el reenvío perfecto. Vea este documento .

Vaughn Cato
fuente
Esto no explica todo sobre por qué _Remove_reference_es necesario. Por ejemplo, si tiene Object&como typedef, y toma una referencia a él, todavía obtiene Object&. ¿Por qué no funciona con &&? No es una respuesta a eso, y tiene que ver con perfecta reenvío.
Nicol Bolas
2
Cierto. Y la respuesta es muy interesante. A & && se reduce a A &, por lo que si intentamos usar (ObjectRef &&) obj1_ref, obtendríamos Object & en este caso.
Vaughn Cato