¿Qué son semánticas de movimiento?

Acabo de terminar de escuchar la entrevista de podcast de radio de Ingeniería de software con Scott Meyers con respecto a C ++ 0x . La mayoría de las nuevas características tienen sentido para mí, y estoy realmente entusiasmado con C ++ 0x ahora, con la excepción de uno. Todavía no tengo semántica de movimientos … ¿Qué son exactamente?

Encuentro que es más fácil entender la semántica de movimientos con código de ejemplo. Comencemos con una clase de cadena muy simple que solo contiene un puntero a un bloque de memoria asignado por el montón:

#include  #include  class string { char* data; public: string(const char* p) { size_t size = strlen(p) + 1; data = new char[size]; memcpy(data, p, size); } 

Como elegimos administrar la memoria nosotros mismos, tenemos que seguir la regla de tres . Voy a diferir la escritura del operador de asignación y solo implementaré el destructor y el constructor de copia por ahora:

  ~string() { delete[] data; } string(const string& that) { size_t size = strlen(that.data) + 1; data = new char[size]; memcpy(data, that.data, size); } 

El constructor de copia define lo que significa copiar objetos de cadena. El parámetro const string& that une a todas las expresiones de tipo cadena que le permite hacer copias en los siguientes ejemplos:

 string a(x); // Line 1 string b(x + y); // Line 2 string c(some_function_returning_a_string()); // Line 3 

Ahora viene la visión clave de la semántica de los movimientos. Tenga en cuenta que solo en la primera línea donde copiamos x es realmente necesaria esta copia profunda, porque es posible que deseemos inspeccionar x más tarde y nos sorprendería mucho si x hubiera cambiado de algún modo. ¿Notaste cómo acabo de decir x tres veces (cuatro veces si incluyes esta oración) y quise decir exactamente el mismo objeto cada vez? Llamamos expresiones como x “lvalues”.

Los argumentos en las líneas 2 y 3 no son lvalues, sino rvalues, porque los objetos de cadena subyacentes no tienen nombres, por lo que el cliente no tiene forma de volver a inspeccionarlos en un momento posterior. Los valores r denotan objetos temporales que se destruyen en el siguiente punto y coma (para ser más precisos: al final de la expresión completa que contiene léxicamente el valor r). Esto es importante porque durante la inicialización de c , pudimos hacer lo que quisiéramos con la cadena fuente, ¡y el cliente no pudo notar la diferencia !

C ++ 0x introduce un nuevo mecanismo llamado “referencia rvalue” que, entre otras cosas, nos permite detectar argumentos rvalue a través de la sobrecarga de funciones. Todo lo que tenemos que hacer es escribir un constructor con un parámetro de referencia rvalue. Dentro de ese constructor podemos hacer lo que queramos con la fuente, siempre que la dejemos en algún estado válido:

  string(string&& that) // string&& is an rvalue reference to a string { data = that.data; that.data = nullptr; } 

¿Qué hemos hecho aquí? En lugar de copiar profundamente los datos del montón, acabamos de copiar el puntero y luego establecer el puntero original en nulo. En efecto, hemos “robado” los datos que originalmente pertenecían a la cadena fuente. Nuevamente, la idea clave es que bajo ninguna circunstancia el cliente podría detectar que la fuente se había modificado. Como realmente no hacemos una copia aquí, llamamos a este constructor un “constructor de movimiento”. Su trabajo es mover recursos de un objeto a otro en lugar de copiarlos.

¡Felicidades, ahora entiendes los conceptos básicos de la semántica de movimientos! Continuemos implementando el operador de asignación. Si no está familiarizado con el idioma de copiar y intercambiar , apréndalo y regrese, porque es un modismo C ++ impresionante relacionado con la seguridad de las excepciones.

  string& operator=(string that) { std::swap(data, that.data); return *this; } }; 

Huh, eso es todo? “¿Dónde está la referencia de valor?” puedes preguntar “¡No lo necesitamos aquí!” es mi respuesta 🙂

Tenga en cuenta que pasamos el parámetro that por valor , por lo that debe inicializarse al igual que cualquier otro objeto de cadena. ¿Exactamente cómo se va a inicializar? En los viejos tiempos de C ++ 98 , la respuesta habría sido “por el constructor de copia”. En C ++ 0x, el comstackdor elige entre el constructor de copia y el constructor de movimiento en función de si el argumento para el operador de asignación es un valor l o un valor r.

Entonces, si dices a = b , el constructor de la copia inicializará that (porque la expresión b es un valor l), y el operador de asignación intercambia los contenidos con una copia profunda recientemente creada. Esa es la definición misma del idioma de copiar y cambiar: haga una copia, intercambie el contenido con la copia y luego elimine la copia dejando el scope. Nada nuevo aquí.

Pero si dices a = x + y , el constructor de movimientos inicializará that (porque la expresión x + y es un valor r), por lo que no hay una copia profunda involucrada, solo un movimiento eficiente. that sigue siendo un objeto independiente del argumento, pero su construcción era trivial, ya que los datos del montón no tenían que ser copiados, simplemente movidos. No fue necesario copiarlo porque x + y es un valor r, y de nuevo, está bien moverse de los objetos de cadena denotados por valores r.

Para resumir, el constructor de copias hace una copia profunda, porque la fuente debe permanecer intacta. El constructor de movimientos, por otro lado, solo puede copiar el puntero y luego establecer el puntero en la fuente en nulo. Está bien “anular” el objeto fuente de esta manera, porque el cliente no tiene manera de inspeccionar el objeto nuevamente.

Espero que este ejemplo tenga el punto principal. Hay mucho más para evaluar las referencias y mover la semántica que intencionalmente dejé para mantenerlo simple. Si desea más información, consulte mi respuesta complementaria .

Mi primera respuesta fue una introducción extremadamente simplificada para mover la semántica, y muchos detalles se dejaron a propósito para mantenerlo simple. Sin embargo, hay mucho más para mover la semántica, y pensé que era hora de una segunda respuesta para llenar los vacíos. La primera respuesta ya es bastante antigua, y no parecía correcto reemplazarla simplemente por un texto completamente diferente. Creo que todavía sirve como primera introducción. Pero si quieres profundizar, sigue leyendo 🙂

Stephan T. Lavavej se tomó el tiempo de proporcionar comentarios valiosos. Muchas gracias, Stephan!

Introducción

La semántica de movimiento permite que un objeto, bajo ciertas condiciones, se apropie de los recursos externos de otros objetos. Esto es importante de dos maneras:

  1. Convirtiendo copias caras en movimientos baratos. Ver mi primera respuesta para un ejemplo. Tenga en cuenta que si un objeto no gestiona al menos un recurso externo (ya sea directa o indirectamente a través de sus objetos miembros), la semántica de movimiento no ofrecerá ninguna ventaja sobre la semántica de copia. En ese caso, copiar un objeto y mover un objeto significa exactamente lo mismo:

     class cannot_benefit_from_move_semantics { int a; // moving an int means copying an int float b; // moving a float means copying a float double c; // moving a double means copying a double char d[64]; // moving a char array means copying a char array // ... }; 
  2. Implementando tipos seguros de “solo mover”; es decir, tipos para los que copiar no tiene sentido, pero mover sí. Los ejemplos incluyen lockings, manejadores de archivos y punteros inteligentes con semántica única de propiedad. Nota: Esta respuesta trata sobre std::auto_ptr , una plantilla de biblioteca estándar desaprobada de C ++ 98, que fue reemplazada por std::unique_ptr en C ++ 11. Los progtwigdores de C ++ intermedios probablemente estén al menos algo familiarizados con std::auto_ptr , y debido a la “semántica de movimiento” que muestra, parece ser un buen punto de partida para discutir la semántica de movimientos en C ++ 11. YMMV.

¿Qué es un movimiento?

La biblioteca estándar C ++ 98 ofrece un puntero inteligente con semántica única de propiedad llamada std::auto_ptr . En caso de que no esté familiarizado con auto_ptr , su propósito es garantizar que siempre se libere un objeto dinámicamente asignado, incluso frente a excepciones:

 { std::auto_ptr a(new Triangle); // ... // arbitrary code, could throw exceptions // ... } // < --- when a goes out of scope, the triangle is deleted automatically 

Lo inusual de auto_ptr es su comportamiento de "copia":

 auto_ptr a(new Triangle); +---------------+ | triangle data | +---------------+ ^ | | | +-----|---+ | +-|-+ | a | p | | | | | +---+ | +---------+ auto_ptr b(a); +---------------+ | triangle data | +---------------+ ^ | +----------------------+ | +---------+ +-----|---+ | +---+ | | +-|-+ | a | p | | | b | p | | | | | +---+ | | +---+ | +---------+ +---------+ 

Observe cómo la inicialización de b con a no copia el triángulo, sino que transfiere la propiedad del triángulo de a a b . También decimos " a se mueve a b " o "el triángulo se mueve de a b ". Esto puede sonar confuso, porque el triángulo siempre permanece en el mismo lugar en la memoria.

Mover un objeto significa transferir la propiedad de algún recurso que administra a otro objeto.

El constructor de copia de auto_ptr probablemente se parece a esto (algo simplificado):

 auto_ptr(auto_ptr& source) // note the missing const { p = source.p; source.p = 0; // now the source no longer owns the object } 

Movimientos peligrosos e inofensivos

Lo peligroso de auto_ptr es que lo que sintácticamente parece una copia es en realidad un movimiento. Intentar llamar a una función miembro en un auto_ptr movido desde invocará un comportamiento no definido, por lo que debe tener mucho cuidado de no usar un auto_ptr después de que se haya movido desde:

 auto_ptr a(new Triangle); // create triangle auto_ptr b(a); // move a into b double area = a->area(); // undefined behavior 

Pero auto_ptr no siempre es peligroso. Las funciones de fábrica son un caso de uso perfectamente auto_ptr para auto_ptr :

 auto_ptr make_triangle() { return auto_ptr(new Triangle); } auto_ptr c(make_triangle()); // move temporary into c double area = make_triangle()->area(); // perfectly safe 

Observe cómo ambos ejemplos siguen el mismo patrón sintáctico:

 auto_ptr variable(expression); double area = expression->area(); 

Y, sin embargo, uno de ellos invoca un comportamiento indefinido, mientras que el otro no. Entonces, ¿cuál es la diferencia entre las expresiones a y make_triangle() ? ¿No son ambos del mismo tipo? De hecho lo son, pero tienen diferentes categorías de valores .

Categorías de valor

Obviamente, debe haber una profunda diferencia entre la expresión a que denota una variable auto_ptr y la expresión make_triangle() que denota la llamada de una función que devuelve un valor auto_ptr , creando así un nuevo objeto temporal auto_ptr cada vez que se llama . a es un ejemplo de un lvalue , mientras que make_triangle() es un ejemplo de un valor de r .

Pasar de valores l como a es peligroso, porque más adelante podríamos tratar de llamar a una función miembro a través de a , invocando un comportamiento indefinido. Por otro lado, pasar de make_triangle() como make_triangle() es perfectamente seguro, porque una vez que el constructor de copia ha hecho su trabajo, no podemos usar el temporal nuevamente. No hay una expresión que denote a dicho temporal; si simplemente escribimos make_triangle() nuevo, obtenemos un temporal diferente . De hecho, el traslado temporal ya se ha ido en la siguiente línea:

 auto_ptr c(make_triangle()); ^ the moved-from temporary dies right here 

Tenga en cuenta que las letras l y r tienen un origen histórico en el lado izquierdo y en el lado derecho de una tarea. Esto ya no es así en C ++, porque hay valores l que no pueden aparecer en el lado izquierdo de una asignación (como matrices o tipos definidos por el usuario sin un operador de asignación), y hay valores que pueden (todos los valores de clase de los tipos con un operador de asignación).

Un valor de clase de clase es una expresión cuya evaluación crea un objeto temporal. En circunstancias normales, ninguna otra expresión dentro del mismo ámbito denota el mismo objeto temporal.

Referencias de Rvalue

Ahora entendemos que pasar de lvalues ​​es potencialmente peligroso, pero pasar de valores es inofensivo. Si C ++ tiene soporte de lenguaje para distinguir argumentos lvalue de argumentos rvalue, podríamos prohibir completamente movernos desde lvalues, o al menos hacer que el movimiento de lvalues ​​sea explícito en el sitio de la llamada, de modo que ya no nos muevamos por accidente.

La respuesta de C ++ 11 a este problema son las referencias de valor r . Una referencia de valor real es un nuevo tipo de referencia que solo se une a valores r, y la syntax es X&& . La buena referencia antigua X& ahora se conoce como una referencia lvalue . (Tenga en cuenta que X&& no es una referencia a una referencia, no existe tal cosa en C ++).

Si lanzamos const a la mezcla, ya tenemos cuatro tipos diferentes de referencias. ¿A qué tipos de expresiones de tipo X pueden vincularse?

  lvalue const lvalue rvalue const rvalue --------------------------------------------------------- X& yes const X& yes yes yes yes X&& yes const X&& yes yes 

En la práctica, puedes olvidarte de const X&& . Estar restringido a leer de valores no es muy útil.

Una referencia rvalue X&& es un nuevo tipo de referencia que solo se une a rvalues.

Conversiones implícitas

Las referencias de Rvalue pasaron por varias versiones. Desde la versión 2.1, una referencia rvalue X&& también se une a todas las categorías de valores de un tipo diferente Y , siempre que exista una conversión implícita de Y a X En ese caso, se crea un temporal de tipo X , y la referencia rvalue se vincula con ese temporal:

 void some_function(std::string&& r); some_function("hello world"); 

En el ejemplo anterior, "hello world" es un lvalue de tipo const char[12] . Como hay una conversión implícita de const char[12] través de const char* a std::string , se crea un temporal de tipo std::string y r se vincula con ese temporal. Este es uno de los casos donde la distinción entre rvalues ​​(expresiones) y temporales (objetos) es un poco borrosa.

Mover constructores

Un ejemplo útil de una función con un parámetro X&& es el constructor de movimiento X::X(X&& source) . Su propósito es transferir la propiedad del recurso gestionado desde la fuente al objeto actual.

En C ++ 11, std::auto_ptr ha sido reemplazado por std::unique_ptr que aprovecha las referencias de rvalue. Desarrollaré y analizaré una versión simplificada de unique_ptr . Primero, encapsulamos un puntero sin formato y sobrecargamos los operadores -> y * , por lo que nuestra clase se siente como un puntero:

 template class unique_ptr { T* ptr; public: T* operator->() const { return ptr; } T& operator*() const { return *ptr; } 

El constructor toma posesión del objeto y el destructor lo elimina:

  explicit unique_ptr(T* p = nullptr) { ptr = p; } ~unique_ptr() { delete ptr; } 

Ahora viene la parte interesante, el constructor de movimientos:

  unique_ptr(unique_ptr&& source) // note the rvalue reference { ptr = source.ptr; source.ptr = nullptr; } 

Este constructor de movimientos hace exactamente lo que hizo el constructor de copia auto_ptr , pero solo se puede suministrar con valores r:

 unique_ptr a(new Triangle); unique_ptr b(a); // error unique_ptr c(make_triangle()); // okay 

La segunda línea no comstack, porque a es un lvalue, pero el parámetro unique_ptr&& source solo puede vincularse a rvalues. Esto es exactamente lo que queríamos; los movimientos peligrosos nunca deben estar implícitos. La tercera línea comstack muy bien, porque make_triangle() es un valor r. El constructor de movimiento transferirá la propiedad del temporal a c . Nuevamente, esto es exactamente lo que queríamos.

El constructor de movimiento transfiere la propiedad de un recurso administrado al objeto actual.

Mover operadores de asignación

La última pieza faltante es el operador de asignación de movimiento. Su trabajo es liberar el recurso anterior y adquirir el nuevo recurso de su argumento:

  unique_ptr& operator=(unique_ptr&& source) // note the rvalue reference { if (this != &source) // beware of self-assignment { delete ptr; // release the old resource ptr = source.ptr; // acquire the new resource source.ptr = nullptr; } return *this; } }; 

Observe cómo esta implementación del operador de asignación de movimiento duplica la lógica tanto del destructor como del constructor de movimiento. ¿Estás familiarizado con el modismo de copiar y cambiar? También se puede aplicar para mover la semántica como la expresión de movimiento e intercambio:

  unique_ptr& operator=(unique_ptr source) // note the missing reference { std::swap(ptr, source.ptr); return *this; } }; 

Ahora que el source es una variable de tipo unique_ptr , será inicializado por el constructor de movimientos; es decir, el argumento se moverá al parámetro. Todavía se requiere que el argumento sea un valor r, porque el propio constructor de movimiento tiene un parámetro de referencia rvalue. Cuando el flujo de control alcanza la llave de cierre del operator= , la source sale del scope, liberando el recurso anterior automáticamente.

El operador de asignación de movimiento transfiere la propiedad de un recurso gestionado al objeto actual, liberando el recurso anterior. La expresión "mover y cambiar" simplifica la implementación.

Moviéndose desde lvalues

A veces, queremos movernos de lvalues. Es decir, a veces queremos que el comstackdor trate un valor l como si fuera un valor r, por lo que puede invocar al constructor de movimiento, aunque podría ser potencialmente inseguro. Para este propósito, C ++ 11 ofrece una plantilla de función de biblioteca estándar llamada std::move dentro del encabezado . Este nombre es un poco desafortunado, porque std::move simplemente arroja un lvalue a un valor r; no mueve nada por sí mismo. Simplemente permite moverse. Tal vez debería haber sido llamado std::cast_to_rvalue o std::enable_move , pero ahora estamos estancados con el nombre.

Aquí es cómo se mueve explícitamente desde un valor l:

 unique_ptr a(new Triangle); unique_ptr b(a); // still an error unique_ptr c(std::move(a)); // okay 

Tenga en cuenta que después de la tercera línea, ya no posee un triángulo. Está bien, porque escribiendo explícitamente std::move(a) , expresamos claramente nuestras intenciones: "Estimado constructor, haga lo que quiera con a para inicializar c ; ya no me importa más. Siéntase libre de tener a tu manera con a ".

std::move(some_lvalue) arroja un lvalue a un valor r, lo que permite un movimiento posterior.

Xvalues

Tenga en cuenta que aunque std::move(a) es un valor r, su evaluación no crea un objeto temporal. Este enigma obligó al comité a introducir una tercera categoría de valor. Algo que puede vincularse a una referencia de valor real, aunque no es un valor r en el sentido tradicional, se denomina xvalor (valor de eXpiring). Los valores r tradicionales se renombraron a prvalues ( valores puras).

Ambos prvalues ​​y xvalues ​​son valores r. Xvalues ​​y lvalues ​​son ambos glvalues (lvalues ​​generalizados). Las relaciones son más fáciles de entender con un diagtwig:

  expressions / \ / \ / \ glvalues rvalues / \ / \ / \ / \ / \ / \ lvalues xvalues prvalues 

Tenga en cuenta que solo los valores x son realmente nuevos; el rest solo se debe al cambio de nombre y agrupamiento.

Los valores de C ++ 98 se conocen como prvalues ​​en C ++ 11. Mentalmente reemplace todas las ocurrencias de "rvalue" en los párrafos anteriores con "prvalue".

Mudanza de funciones

Hasta ahora, hemos visto el movimiento en variables locales y en parámetros de funciones. Pero moverse también es posible en la dirección opuesta. Si una función retorna por valor, algún objeto en el sitio de llamada (probablemente una variable local o temporal, pero podría ser cualquier tipo de objeto) se inicializa con la expresión después de la instrucción return como argumento para el constructor de movimientos:

 unique_ptr make_triangle() { return unique_ptr(new Triangle); } \-----------------------------/ | | temporary is moved into c | v unique_ptr c(make_triangle()); 

Quizás sorprendentemente, los objetos automáticos (variables locales que no están declaradas como static ) también se pueden mover implícitamente fuera de las funciones:

 unique_ptr make_square() { unique_ptr result(new Square); return result; // note the missing std::move } 

¿Cómo es que el constructor de movimientos acepta el result lvalue como argumento? El scope del result está a punto de terminar, y se destruirá durante el desenrollado de la stack. Nadie podría quejarse después de que el result haya cambiado de alguna manera; cuando el flujo de control regresa a la persona que llama, ¡el result ya no existe! Por esa razón, C ++ 11 tiene una regla especial que permite devolver objetos automáticos de funciones sin tener que escribir std::move . De hecho, nunca debe usar std::move para mover objetos automáticos de las funciones, ya que esto inhibe la "optimización del valor de retorno denominado" (NRVO).

Nunca use std::move para mover objetos automáticos fuera de las funciones.

Tenga en cuenta que en ambas funciones de fábrica, el tipo de devolución es un valor, no una referencia de valor r. Las referencias de Rvalue todavía son referencias y, como siempre, nunca debe devolver una referencia a un objeto automático; la persona que llama terminaría con una referencia colgante si engañó al comstackdor para que aceptara su código, así:

 unique_ptr&& flawed_attempt() // DO NOT DO THIS! { unique_ptr very_bad_idea(new Square); return std::move(very_bad_idea); // WRONG! } 

Nunca devuelva objetos automáticos por referencia de valor real. El movimiento se realiza exclusivamente por el constructor de movimiento, no por std::move , y no simplemente vinculando un valor r a una referencia rvalue.

Mudarse a los miembros

Tarde o temprano, vas a escribir un código como este:

 class Foo { unique_ptr member; public: Foo(unique_ptr&& parameter) : member(parameter) // error {} }; 

Básicamente, el comstackdor se quejará de que el parameter es un valor l. Si nos fijamos en su tipo, verá una referencia rvalue, pero una referencia rvalue simplemente significa "una referencia que está vinculada a un valor r"; ¡ no significa que la referencia en sí sea un valor r! De hecho, el parameter es solo una variable ordinaria con un nombre. Puede usar el parameter tantas veces como quiera dentro del cuerpo del constructor, y siempre denota el mismo objeto. Irse implícitamente sería peligroso, por lo tanto, el lenguaje lo prohíbe.

Una referencia de valor r nombrada es un valor l, al igual que cualquier otra variable.

La solución es habilitar manualmente el movimiento:

 class Foo { unique_ptr member; public: Foo(unique_ptr&& parameter) : member(std::move(parameter)) // note the std::move {} }; 

Podría argumentar que ese parameter ya no se usa después de la inicialización del member . ¿Por qué no hay una regla especial para insertar silenciosamente std::move igual que con los valores devueltos? Probablemente porque sería una carga excesiva para los implementadores del comstackdor. Por ejemplo, ¿qué pasa si el cuerpo constructor estaba en otra unidad de traducción? Por el contrario, la regla de valor de retorno simplemente tiene que verificar las tablas de símbolos para determinar si el identificador después de la palabra clave de return denota o no un objeto automático.

También puede pasar parameter por valor. Para los tipos de solo movimiento como unique_ptr , parece que todavía no hay un idioma establecido. Personalmente, prefiero pasar de valor, ya que causa menos desorden en la interfaz.

Funciones especiales de miembro

C ++ 98 declara implícitamente tres funciones de miembros especiales a petición, es decir, cuando se necesitan en alguna parte: el constructor de copia, el operador de asignación de copia y el destructor.

 X::X(const X&); // copy constructor X& X::operator=(const X&); // copy assignment operator X::~X(); // destructor 

Las referencias de Rvalue pasaron por varias versiones. Desde la versión 3.0, C ++ 11 declara dos funciones especiales adicionales a pedido: el constructor de movimiento y el operador de asignación de movimiento. Tenga en cuenta que ni VC10 ni VC11 se ajustan a la versión 3.0 todavía, por lo que tendrá que implementarlos usted mismo.

 X::X(X&&); // move constructor X& X::operator=(X&&); // move assignment operator 

Estas dos nuevas funciones miembro especiales solo se declaran implícitamente si ninguna de las funciones miembro especiales se declara manualmente. Además, si declara su propio constructor de movimiento o operador de asignación de movimiento, ni el constructor de copia ni el operador de asignación de copia serán declarados implícitamente.

¿Qué significan estas reglas en la práctica?

Si escribe una clase sin recursos no administrados, no es necesario que declare ninguna de las cinco funciones especiales miembro, y obtendrá la semántica de copia correcta y la semántica de movimiento de forma gratuita. De lo contrario, tendrá que implementar las funciones especiales para miembros usted mismo. Por supuesto, si su clase no se beneficia de la semántica de movimientos, no es necesario implementar las operaciones de movimientos especiales.

Tenga en cuenta que el operador de asignación de copia y el operador de asignación de movimiento se pueden fusionar en un único operador de asignación unificada, tomando su argumento por valor:

 X& X::operator=(X source) // unified assignment operator { swap(source); // see my first answer for an explanation return *this; } 

De esta forma, la cantidad de funciones especiales para miembros se reduce de cinco a cuatro. Aquí hay una compensación entre la excepción, la seguridad y la eficiencia, pero no soy un experto en este tema.

Reenvío de referencias ( anteriormente conocidas como referencias universales )

Considere la siguiente plantilla de función:

 template void foo(T&&); 

Puede esperar que T&& solo se vincule con valores r, porque a primera vista parece una referencia de valor real. Sin embargo, como resulta, T&& también se une a lvalues:

 foo(make_triangle()); // T is unique_ptr, T&& is unique_ptr&& unique_ptr a(new Triangle); foo(a); // T is unique_ptr&, T&& is unique_ptr& 

Si el argumento es un valor r de tipo X , T se deduce que es X , por lo tanto, T&& significa X&& . Esto es lo que cualquiera esperaría. Pero si el argumento es un valor l de tipo X , debido a una regla especial, se deduce que T es X& , por lo tanto, T&& significaría algo como X& && . Pero dado que C ++ todavía no tiene ninguna noción de referencias a referencias, el tipo X& && está colapsado en X& . Esto puede sonar confuso e inútil al principio, pero el colapso de referencia es esencial para un reenvío perfecto (que no se discutirá aquí).

T && no es una referencia rvalue, sino una referencia de reenvío. También se une a lvalues, en cuyo caso T y T&& son ambas referencias lvalue.

Si desea restringir una plantilla de función a rvalues, puede combinar SFINAE con rasgos de tipo:

 #include  template typename std::enable_if::value, void>::type foo(T&&); 

Implementación de movimiento

Ahora que comprende el colapso de referencias, aquí está cómo se implementa std::move :

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

Como puede ver, move acepta cualquier tipo de parámetro gracias a la referencia de reenvío T&& , y devuelve una referencia de valor r. La llamada de meta-función std::remove_reference::type es necesaria porque, de lo contrario, para lvalues ​​de tipo X , el tipo de retorno sería X& && , que colapsaría en X& . Como t es siempre un valor l (recuerde que una referencia de valor r nombrada es un valor l), pero queremos vincular t a una referencia de valor r, tenemos que convertir explícitamente t en el tipo de retorno correcto. La llamada de una función que devuelve una referencia rvalue es en sí misma un xvalor. Ahora sabes de dónde vienen los valores x;)

La invocación de una función que devuelve una referencia rvalue, como std::move , es un xvalor.

Tenga en cuenta que return by rvalue reference está bien en este ejemplo, porque t no denota un objeto automático, sino un objeto que fue pasado por la persona que llama.

La semántica de movimiento se basa en referencias rvalue .
Un rvalue es un objeto temporal, que se destruirá al final de la expresión. En C ++ actual, los valores r solo se unen a las referencias const . C ++ 1x permitirá referencias de valores no const , deletreados T&& , que son referencias a un objeto rvalue.
Como un valor r va a morir al final de una expresión, puede robar sus datos . En lugar de copiarlo en otro objeto, mueve sus datos en él.

 class X { public: X(X&& rhs) // ctor taking an rvalue reference, so-called move-ctor : data_() { // since 'x' is an rvalue object, we can steal its data this->swap(std::move(rhs)); // this will leave rhs with the empty data } void swap(X&& rhs); // ... }; // ... X f(); X x = f(); // f() returns result as rvalue, so this calls move-ctor 

En el código anterior, con comstackdores antiguos, el resultado de f() se copia en x utilizando el constructor de copias de X Si su comstackdor admite semántica de movimiento y X tiene un constructor de movimiento, entonces se llama en su lugar. Como su argumento rhs es un valor r , sabemos que ya no es necesario y podemos robar su valor.
Entonces, el valor se mueve del temporal sin nombre devuelto de f() a x (mientras que los datos de x , inicializados a una X vacía, se mueven al temporal, que se destruirá después de la asignación).

Supongamos que tiene una función que devuelve un objeto sustancial:

 Matrix multiply(const Matrix &a, const Matrix &b); 

Cuando escribes código como este:

 Matrix r = multiply(a, b); 

entonces un comstackdor ordinario de C ++ creará un objeto temporal para el resultado de multiply() , llamará al constructor de copia para inicializar r , y luego destruirá el valor de retorno temporal. La semántica de movimiento en C ++ 0x permite llamar al “constructor de movimiento” para inicializar r copiando su contenido, y luego descartar el valor temporal sin tener que destruirlo.

Esto es especialmente importante si (como quizás el ejemplo de Matrix anterior), el objeto que se está copiando asigna memoria adicional en el montón para almacenar su representación interna. Un constructor de copia tendría que hacer una copia completa de la representación interna, o usar el recuento de referencias y la semántica de escritura-escritura entre ellos. Un constructor de movimientos dejaría sola la memoria del montón y simplemente copia el puntero dentro del objeto Matrix .

If you are really interested in a good, in-depth explanation of move semantics, I’d highly recommend reading the original paper on them, “A Proposal to Add Move Semantics Support to the C++ Language.”

It’s very accessible and easy to read and it makes an excellent case for the benefits that they offer. There are other more recent and up to date papers about move semantics available on the WG21 website , but this one is probably the most straightforward since it approaches things from a top-level view and doesn’t get very much into the gritty language details.

Move semantics is about transferring resources rather than copying them when nobody needs the source value anymore.

In C++03, objects are often copied, only to be destroyed or assigned-over before any code uses the value again. For example, when you return by value from a function—unless RVO kicks in—the value you’re returning is copied to the caller’s stack frame, and then it goes out of scope and is destroyed. This is just one of many examples: see pass-by-value when the source object is a temporary, algorithms like sort that just rearrange items, reallocation in vector when its capacity() is exceeded, etc.

When such copy/destroy pairs are expensive, it’s typically because the object owns some heavyweight resource. For example, vector may own a dynamically-allocated memory block containing an array of string objects, each with its own dynamic memory. Copying such an object is costly: you have to allocate new memory for each dynamically-allocated blocks in the source, and copy all the values across. Then you need deallocate all that memory you just copied. However, moving a large vector means just copying a few pointers (that refer to the dynamic memory block) to the destination and zeroing them out in the source.

In easy (practical) terms:

Copying an object means copying its “static” members and calling the new operator for its dynamic objects. ¿Derecha?

 class A { int i, *p; public: A(const A& a) : i(ai), p(new int(*ap)) {} ~A() { delete p; } }; 

However, to move an object (I repeat, in a practical point of view) implies only to copy the pointers of dynamic objects, and not to create new ones.

But, is that not dangerous? Of course, you could destruct a dynamic object twice (segmentation fault). So, to avoid that, you should “invalidate” the source pointers to avoid destructing them twice:

 class A { int i, *p; public: // Movement of an object inside a copy constructor. A(const A& a) : i(ai), p(ap) { ap = nullptr; // pointer invalidated. } ~A() { delete p; } // Deleting NULL, 0 or nullptr (address 0x0) is safe. }; 

Ok, but if I move an object, the source object becomes useless, no? Of course, but in certain situations that’s very useful. The most evident one is when I call a function with an anonymous object (temporal, rvalue object, …, you can call it with different names):

 void heavyFunction(HeavyType()); 

In that situation, an anonymous object is created, next copied to the function parameter, and afterwards deleted. So, here it is better to move the object, because you don’t need the anonymous object and you can save time and memory.

This leads to the concept of an “rvalue” reference. They exist in C++11 only to detect if the received object is anonymous or not. I think you do already know that an “lvalue” is an assignable entity (the left part of the = operator), so you need a named reference to an object to be capable to act as an lvalue. A rvalue is exactly the opposite, an object with no named references. Because of that, anonymous object and rvalue are synonyms. Asi que:

 class A { int i, *p; public: // Copy A(const A& a) : i(ai), p(new int(*ap)) {} // Movement (&& means "rvalue reference to") A(A&& a) : i(ai), p(ap) { ap = nullptr; } ~A() { delete p; } }; 

In this case, when an object of type A should be “copied”, the compiler creates a lvalue reference or a rvalue reference according to if the passed object is named or not. When not, your move-constructor is called and you know the object is temporal and you can move its dynamic objects instead of copying them, saving space and memory.

It is important to remember that “static” objects are always copied. There’s no ways to “move” a static object (object in stack and not on heap). So, the distinction “move”/ “copy” when an object has no dynamic members (directly or indirectly) is irrelevant.

If your object is complex and the destructor has other secondary effects, like calling to a library’s function, calling to other global functions or whatever it is, perhaps is better to signal a movement with a flag:

 class Heavy { bool b_moved; // staff public: A(const A& a) { /* definition */ } A(A&& a) : // initialization list { a.b_moved = true; } ~A() { if (!b_moved) /* destruct object */ } }; 

So, your code is shorter (you don’t need to do a nullptr assignment for each dynamic member) and more general.

Other typical question: what is the difference between A&& and const A&& ? Of course, in the first case, you can modify the object and in the second not, but, practical meaning? In the second case, you can’t modify it, so you have no ways to invalidate the object (except with a mutable flag or something like that), and there is no practical difference to a copy constructor.

And what is perfect forwarding ? It is important to know that a “rvalue reference” is a reference to a named object in the “caller’s scope”. But in the actual scope, a rvalue reference is a name to an object, so, it acts as a named object. If you pass an rvalue reference to another function, you are passing a named object, so, the object isn’t received like a temporal object.

 void some_function(A&& a) { other_function(a); } 

The object a would be copied to the actual parameter of other_function . If you want the object a continues being treated as a temporary object, you should use the std::move function:

 other_function(std::move(a)); 

With this line, std::move will cast a to an rvalue and other_function will receive the object as a unnamed object. Of course, if other_function has not specific overloading to work with unnamed objects, this distinction is not important.

Is that perfect forwarding? Not, but we are very close. Perfect forwarding is only useful to work with templates, with the purpose to say: if I need to pass an object to another function, I need that if I receive a named object, the object is passed as a named object, and when not, I want to pass it like a unnamed object:

 template void some_function(T&& a) { other_function(std::forward(a)); } 

That’s the signature of a prototypical function that uses perfect forwarding, implemented in C++11 by means of std::forward . This function exploits some rules of template instantiation:

  `A& && == A&` `A&& && == A&&` 

So, if T is a lvalue reference to A ( T = A&), a also ( A& && => A&). If T is a rvalue reference to A , a also (A&& && => A&&). In both cases, a is a named object in the actual scope, but T contains the information of its “reference type” from the caller scope’s point of view. This information ( T ) is passed as template parameter to forward and ‘a’ is moved or not according to the type of T .

It’s like copy semantics, but instead of having to duplicate all of the data you get to steal the data from the object being “moved” from.

You know what a copy semantics means right? it means you have types which are copyable, for user-defined types you define this either buy explicitly writing a copy constructor & assignment operator or the compiler generates them implicitly. This will do a copy.

Move semantics is basically a user-defined type with constructor that takes an r-value reference (new type of reference using && (yes two ampersands)) which is non-const, this is called a move constructor, same goes for assignment operator. So what does a move constructor do, well instead of copying memory from it’s source argument it ‘moves’ memory from the source to the destination.

When would you want to do that? well std::vector is an example, say you created a temporary std::vector and you return it from a function say:

 std::vector get_foos(); 

You’re going to have overhead from the copy constructor when the function returns, if (and it will in C++0x) std::vector has a move constructor instead of copying it can just set it’s pointers and ‘move’ dynamically allocated memory to the new instance. It’s kind of like transfer-of-ownership semantics with std::auto_ptr.

To illustrate the need for move semantics , let’s consider this example without move semantics:

Here’s a function that takes an object of type T and returns an object of the same type T :

 T f(T o) { return o; } //^^^ new object constructed 

The above function uses call by value which means that when this function is called an object must be constructed to be used by the function.
Because the function also returns by value , another new object is constructed for the return value:

 T b = f(a); //^ new object constructed 

Two new objects have been constructed, one of which is a temporary object that’s only used for the duration of the function.

When the new object is created from the return value, the copy constructor is called to copy the contents of the temporary object to the new object b. After the function completes, the temporary object used in the function goes out of scope and is destroyed.


Now, let’s consider what a copy constructor does.

It must first initialize the object, then copy all the relevant data from the old object to the new one.
Depending on the class, maybe its a container with very much data, then that could represent much time and memory usage

 // Copy constructor T::T(T &old) { copy_data(m_a, old.m_a); copy_data(m_b, old.m_b); copy_data(m_c, old.m_c); } 

With move semantics it’s now possible to make most of this work less unpleasant by simply moving the data rather than copying.

 // Move constructor T::T(T &&old) noexcept { m_a = std::move(old.m_a); m_b = std::move(old.m_b); m_c = std::move(old.m_c); } 

Moving the data involves re-associating the data with the new object. And no copy takes place at all.

This is accomplished with an rvalue reference.
An rvalue reference works pretty much like an lvalue reference with one important difference:
an rvalue reference can be moved and an lvalue cannot.

From cppreference.com :

To make strong exception guarantee possible, user-defined move constructors should not throw exceptions. In fact, standard containers typically rely on std::move_if_noexcept to choose between move and copy when container elements need to be relocated. If both copy and move constructors are provided, overload resolution selects the move constructor if the argument is an rvalue (either a prvalue such as a nameless temporary or an xvalue such as the result of std::move), and selects the copy constructor if the argument is an lvalue (named object or a function/operator returning lvalue reference). If only the copy constructor is provided, all argument categories select it (as long as it takes a reference to const, since rvalues can bind to const references), which makes copying the fallback for moving, when moving is unavailable. In many situations, move constructors are optimized out even if they would produce observable side-effects, see copy elision. A constructor is called a ‘move constructor’ when it takes an rvalue reference as a parameter. It is not obligated to move anything, the class is not required to have a resource to be moved and a ‘move constructor’ may not be able to move a resource as in the allowable (but maybe not sensible) case where the parameter is a const rvalue reference (const T&&).

I’m writing this to make sure I understand it properly.

Move semantics were created to avoid the unnecessary copying of large objects. Bjarne Stroustrup in his book “The C++ Programming Language” uses two examples where unnecessary copying occurs by default: one, the swapping of two large objects, and two, the returning of a large object from a method.

Swapping two large objects usually involves copying the first object to a temporary object, copying the second object to the first object, and copying the temporary object to the second object. For a built-in type, this is very fast, but for large objects these three copies could take a large amount of time. A “move assignment” allows the programmer to override the default copy behavior and instead swap references to the objects, which means that there is no copying at all and the swap operation is much faster. The move assignment can be invoked by calling the std::move() method.

Returning an object from a method by default involves making a copy of the local object and its associated data in a location which is accessible to the caller (because the local object is not accessible to the caller and disappears when the method finishes). When a built-in type is being returned, this operation is very fast, but if a large object is being returned, this could take a long time. The move constructor allows the programmer to override this default behavior and instead “reuse” the heap data associated with the local object by pointing the object being returned to the caller to heap data associated with the local object. Thus no copying is required.

In languages which do not allow the creation of local objects (that is, objects on the stack) these types of problems do not occur as all objects are allocated on the heap and are always accessed by reference.