c++ c++-faq c++11 move-semantics

c++ - ¿Qué son las semánticas de movimiento?



c++-faq c++11 (11)

Las semánticas de movimiento se basan en referencias de valores .
Un valor de r es un objeto temporal, que se va a destruir al final de la expresión. En C ++ actual, los rvalues ​​solo se unen a las referencias const . C ++ 1x permitirá referencias de valores no const , deletreados T&& , que son referencias a objetos de valores de valores.
Como un valor va a morir al final de una expresión, puedes robar sus datos . En lugar de copiarlo en otro objeto, mueves 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 compiladores antiguos, el resultado de f() se copia en x utilizando el constructor de copia de X Si su compilador soporta la semántica de movimiento y X tiene un constructor de movimiento, entonces se llama a eso. Dado que su argumento rhs es un valor , sabemos que ya no es necesario y podemos robar su valor.
Por lo tanto, 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).

Acabo de escuchar la entrevista del podcast de radio de Ingeniería de Software con Scott Meyers sobre C++0x . La mayoría de las nuevas funciones tenían sentido para mí, y estoy realmente entusiasmado con C ++ 0x ahora, con la excepción de una. Sigo sin obtener semántica de movimiento ... ¿Qué son exactamente?


Me resulta 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 al montón:

#include <cstring> #include <algorithm> 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, debemos seguir la regla de tres . Voy a aplazar 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 enlaza a todas las expresiones de tipo string 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 idea clave de la semántica del movimiento. Tenga en cuenta que solo en la primera línea donde copiamos x es realmente necesaria esta copia profunda, porque es posible que desee inspeccionar x más tarde y nos sorprendería mucho si x hubiera cambiado de alguna manera. ¿Notó cómo acabo de decir x tres veces (cuatro veces si incluye esta oración) y significó 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 inspeccionarlos nuevamente en un momento posterior. los valores de valores 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 nominal). Esto es importante porque durante la inicialización de c , podríamos hacer lo que quisiéramos con la cadena fuente, ¡y el cliente no pudo notar una diferencia !

C ++ 0x introduce un nuevo mecanismo llamado "referencia de valor" que, entre otras cosas, nos permite detectar argumentos de valor 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 lo 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. Una vez más, la idea clave es que bajo ninguna circunstancia el cliente podría detectar que la fuente había sido modificada. Ya que 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 lo básico de la semántica de movimientos! Continuemos implementando el operador de asignación. Si no está familiarizado con la copia y el cambio de idioma , apréndalo y regrese, porque es un increíble lenguaje de C ++ 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?" usted podría preguntar "¡No lo necesitamos aquí!" es mi respuesta :)

Tenga en cuenta que pasamos el parámetro por valor , por lo that debe inicializarse como 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 compilador elige entre el constructor de copia y el constructor de movimiento en función de si el argumento del operador de asignación es un lvalue o un rvalue.

Entonces, si dice a = b , el constructor de copia lo inicializará (porque la expresión b es un valor l), y el operador de asignación intercambia el contenido con una copia profunda recién creada. Esa es la definición misma del lenguaje de copia e intercambio: haga una copia, intercambie el contenido con la copia y luego elimine la copia dejando el alcance. Nada nuevo aquí.

Pero si dice a = x + y , el constructor de movimientos lo inicializará (porque la expresión x + y es un valor r), por lo que no hay una copia profunda involucrada, solo un movimiento eficiente. that es todavía un objeto independiente del argumento, pero su construcción fue trivial, ya que los datos del montón no tenían que copiarse, simplemente moverse. No fue necesario copiarlo porque x + y es un valor de r, y una vez más, está bien moverse desde objetos de cadena denotados por valores de 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 forma de inspeccionar el objeto nuevamente.

Espero que este ejemplo haya captado el punto principal. Hay mucho más para valorar las referencias y mover la semántica que intencionalmente omití para que sea simple. Si desea más detalles, 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 que sea 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 se sentía bien simplemente reemplazarla con un texto completamente diferente. Creo que todavía sirve como una primera introducción. Pero si quieres profundizar más, sigue leyendo :)

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

Introducción

La semántica de movimiento permite que un objeto, bajo ciertas condiciones, se haga cargo de los recursos externos de algún otro objeto. Esto es importante de dos maneras:

  1. Convertir copias caras en movimientos baratos. Vea mi primera respuesta para un ejemplo. Tenga en cuenta que si un objeto no administra al menos un recurso externo (ya sea directa o indirectamente a través de sus objetos miembros), mover la semántica 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 de "solo movimiento" seguros; es decir, los tipos para los que copiar no tiene sentido, pero el movimiento sí. Los ejemplos incluyen bloqueos, manejadores de archivos y punteros inteligentes con semántica de propiedad única. Nota: Esta respuesta trata sobre std::auto_ptr , una plantilla de biblioteca estándar de C ++ 98 en desuso, que fue reemplazada por std::unique_ptr en C ++ 11. Los programadores de C ++ intermedios probablemente estén al menos algo familiarizados con std::auto_ptr , y debido a la "semántica de movimientos" 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 de C ++ 98 ofrece un puntero inteligente con semántica de propiedad única llamada std::auto_ptr<T> . En caso de que no esté familiarizado con auto_ptr , su propósito es garantizar que un objeto asignado dinámicamente siempre se libere, incluso ante excepciones:

{ std::auto_ptr<Shape> 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<Shape> a(new Triangle); +---------------+ | triangle data | +---------------+ ^ | | | +-----|---+ | +-|-+ | a | p | | | | | +---+ | +---------+ auto_ptr<Shape> 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 que " a se mueve a b " o "el triángulo se mueve de 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 parece sintácticamente una copia es en realidad un movimiento. Intentar llamar a una función miembro en un auto_ptr movido desde invocará un comportamiento indefinido, por lo que debe tener mucho cuidado de no usar un auto_ptr después de que se haya movido de:

auto_ptr<Shape> a(new Triangle); // create triangle auto_ptr<Shape> 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<Shape> make_triangle() { return auto_ptr<Shape>(new Triangle); } auto_ptr<Shape> c(make_triangle()); // move temporary into c double area = make_triangle()->area(); // perfectly safe

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

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

Y, sin embargo, uno de ellos invoca un comportamiento indefinido, mientras que el otro no lo hace. 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 alguna diferencia profunda 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 auto_ptr por valor, creando así un nuevo objeto auto_ptr temporal cada vez que se llama . a es un ejemplo de un lvalue , mientras que make_triangle() es un ejemplo de un rvalue .

Pasar de valores como l es peligroso, porque más tarde podríamos intentar 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 después de que el constructor de copias haya hecho su trabajo, no podemos volver a usar el temporal. No hay expresión que denote dicho temporal; Si simplemente escribimos make_triangle() nuevamente, obtenemos un temporal diferente . De hecho, el temporal movido ya se ha ido en la siguiente línea:

auto_ptr<Shape> 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 cierto en C ++, porque hay valores de 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 los tipos de clase con un operador de asignación).

Un valor de tipo 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 valor

Ahora entendemos que moverse de valores l es potencialmente peligroso, pero pasar de valores es inofensivo. Si C ++ tuviera soporte de lenguaje para distinguir los argumentos de valores de valores de los argumentos de valores de valores, podríamos prohibir completamente el traslado de valores de valores o, al menos, hacer que los valores de valores explícitos en el sitio de la llamada, para que no nos movamos por accidente.

La respuesta de C ++ 11 a este problema son las referencias de valor . Una referencia rvalue es un nuevo tipo de referencia que solo se enlaza con rvalues, y la sintaxis es X&& . La buena referencia antigua X& ahora se conoce como una referencia de valor l . (Tenga en cuenta que X&& no es una referencia a una referencia; no existe tal cosa en C ++).

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

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 de rvalor X&& es un nuevo tipo de referencia que solo se enlaza con rvalues.

Conversiones implícitas

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

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

En el ejemplo anterior, "hello world" es un valor 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 , r está vinculado a ese temporal. Este es uno de los casos en que la distinción entre rvalores (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 movimientos X::X(X&& source) . Su propósito es transferir la propiedad del recurso administrado desde la fuente al objeto actual.

En C ++ 11, std::auto_ptr<T> se ha reemplazado por std::unique_ptr<T> que aprovecha las referencias de valor de r. Desarrollaré y discutiré una versión simplificada de unique_ptr . Primero, encapsulamos un puntero en bruto y sobrecargamos a los operadores -> y * , por lo que nuestra clase se siente como un puntero:

template<typename T> 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:

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

La segunda línea no se compila, 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 ser implícitos. La tercera línea compila bien, porque make_triangle() es un rvalue. El constructor de movimientos transferirá la propiedad de lo temporal a c . Una vez más, esto es exactamente lo que queríamos.

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

Mover operadores de asignación

La última pieza que falta es el operador de asignación de movimiento. Su trabajo es liberar el recurso antiguo 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 movimientos duplica la lógica del destructor y del constructor de movimientos. ¿Está familiarizado con el lenguaje de copia e intercambio? También se puede aplicar para mover la semántica como el lenguaje de movimiento e intercambio:

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

Ahora que la source es una variable de tipo unique_ptr , será inicializada por el constructor de movimiento; es decir, el argumento se moverá al parámetro. Aún se requiere que el argumento sea un rvalue, porque el propio constructor de movimiento tiene un parámetro de referencia rvalue. Cuando el flujo de control llega a la llave de cierre del operator= , la source queda fuera del alcance, liberando el recurso antiguo automáticamente.

El operador de asignación de movimiento transfiere la propiedad de un recurso administrado al objeto actual, liberando el recurso anterior. El lenguaje de movimiento e intercambio simplifica la implementación.

Moviéndose desde lvalues

A veces, queremos pasar de los valores. Es decir, a veces queremos que el compilador trate un valor l como si fuera un valor r, por lo que puede invocar al constructor de movimientos, 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 <utility> . Este nombre es un poco desafortunado, porque std::move simplemente convierte un lvalue a un rvalue; No mueve nada por sí mismo. Simplemente permite moverse. Tal vez debería haber sido nombrado std::cast_to_rvalue o std::enable_move , pero estamos atascados con el nombre por ahora.

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

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

Tenga en cuenta que después de la tercera línea, a ya no posee un triángulo. Está bien, porque al escribir explícitamente std::move(a) , dejamos en claro nuestras intenciones: "Estimado constructor, haz lo que quieras con a inicialización de c ; ya no me importa. Siéntete libre de tener a tu manera con a ".

std::move(some_lvalue) un lvalue en un rvalue, permitiendo así un movimiento posterior.

Valores de x

Tenga en cuenta que aunque std::move(a) es un rvalue, su evaluación no crea un objeto temporal. Este enigma forzó al comité a introducir una tercera categoría de valor. Algo que puede vincularse a una referencia rvalue, aunque no sea un valor rval en el sentido tradicional, se denomina xvalue (valor eXpiring). Los valores tradicionales fueron renombrados a prvalues ( valores puros).

Tanto prvalues ​​como xvalues ​​son rvalues. Los valores de X y valores de l son valores de ambos valores (valores de valores generalizados). Las relaciones son más fáciles de entender con un diagrama:

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

Tenga en cuenta que solo los valores de x son realmente nuevos; El resto se debe simplemente a renombrar y agrupar.

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

Salir de funciones

Hasta ahora, hemos visto movimiento en variables locales y en parámetros de función. Pero el movimiento también es posible en la dirección opuesta. Si una función devuelve por valor, algún objeto en el sitio de la 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 declaración de return como un argumento para el constructor de movimiento:

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

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

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

¿Por qué el constructor Move acepta el result lvalue como un argumento? El alcance del result está a punto de finalizar y se destruirá durante el desenrollado de la pila. Nadie podría quejarse después de que el result hubiera 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 desde funciones sin tener que escribir std::move . De hecho, nunca debe usar std::move para mover objetos automáticos fuera de las funciones, ya que esto inhibe la "optimización del valor de retorno nombrado" (NRVO).

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

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

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

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

Pasando a los miembros

Tarde o temprano, escribirás un código como este:

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

Básicamente, el compilador se quejará de que el parameter es un lvalue. Si observa su tipo, verá una referencia rvalue, pero una referencia rvalue simplemente significa "una referencia que está vinculada a un rvalue"; ¡Esto no significa que la referencia en sí misma sea un valor! De hecho, el parameter es solo una variable ordinaria con un nombre. Puedes usar el parameter tantas veces como quieras dentro del cuerpo del constructor, y siempre denota el mismo objeto. Moverse implícitamente de ello sería peligroso, de ahí que el lenguaje lo prohíba.

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

La solución es habilitar manualmente el movimiento:

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

Podría argumentar que el 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 de retorno? Probablemente porque sería una carga demasiado grande para los implementadores del compilador. Por ejemplo, ¿qué pasa si el cuerpo del 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 un objeto automático.

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

Funciones especiales para miembros

C ++ 98 declara implícitamente tres funciones miembro especiales a pedido, es decir, cuando se necesitan en algún lugar: 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 valores pasaron por varias versiones. Desde la versión 3.0, C ++ 11 declara dos funciones miembro especiales adicionales a pedido: el constructor de movimientos y el operador de asignación de movimientos. Tenga en cuenta que ni VC10 ni VC11 cumplen con la versión 3.0, por lo que tendrá que implementarlas 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 su 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 de miembro especiales, 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 de miembro 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 manera, el número de funciones miembro especiales para implementar cae de cinco a cuatro. Hay una compensación entre excepción-seguridad y eficiencia aquí, pero no soy un experto en este tema.

Reenvío de referencias ( previously conocido como referencias universales )

Considere la siguiente plantilla de función:

template<typename T> void foo(T&&);

Se podría esperar que T&& solo se vincule con valores, porque a primera vista, parece una referencia de valor. Sin embargo, resulta que T&& también se enlaza a los valores l:

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

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

T && no es una referencia de valor, sino una referencia de reenvío. También se une a los valores l, en cuyo caso T y T&& son ambas referencias de valor.

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

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

Implementación de movimiento

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

template<typename T> typename std::remove_reference<T>::type&& move(T&& t) { return static_cast<typename std::remove_reference<T>::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 de r. La llamada meta-función std::remove_reference<T>::type es necesaria porque, de lo contrario, para lvalues ​​de tipo X , el tipo de retorno sería X& && , que colapsaría en X& . Dado que t es siempre un valor de l (recuerde que una referencia de valor de r es un valor de l), pero queremos vincular t a una referencia de valor de r, debemos convertir explícitamente t al tipo de retorno correcto. La llamada de una función que devuelve una referencia rvalue es en sí misma un xvalue. Ahora sabes de donde vienen los valores de xvalues;)

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

Tenga en cuenta que devolver en una referencia rvalue está bien en este ejemplo, porque t no denota un objeto automático, sino un objeto que fue pasado por el llamador.


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);

luego, un compilador de C ++ ordinario 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 que se llame 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 Matrix anterior), el objeto que se está copiando asigna memoria adicional en el montón para almacenar su representación interna. Un constructor de copias tendría que hacer una copia completa de la representación interna, o utilizar el conteo de referencias y la semántica de copia en escritura de manera interina. Un constructor de movimientos dejaría la memoria del montón solo y simplemente copiaría el puntero dentro del objeto Matrix .


Mover la semántica consiste en transferir recursos en lugar de copiarlos cuando ya nadie necesita el valor de origen.

En C ++ 03, los objetos a menudo se copian, solo para ser destruidos o asignados antes de que cualquier código vuelva a usar el valor. Por ejemplo, cuando regresa por valor desde una función, a menos que RVO se active, el valor que está devolviendo se copia al marco de pila de la persona que llama, y ​​luego se sale del alcance y se destruye. Este es solo uno de los muchos ejemplos: ver el paso por valor cuando el objeto fuente es temporal, algoritmos como el sortque simplemente reorganizan los elementos, la reasignación vectorcuando capacity()se excede, etc.

Cuando tales pares de copiar / destruir son caros, generalmente es porque el objeto posee algún recurso de peso pesado. Por ejemplo, vector<string>puede poseer un bloque de memoria asignado dinámicamente que contiene una matriz de stringobjetos, cada uno con su propia memoria dinámica. Copiar un objeto de este tipo es costoso: debe asignar una nueva memoria para cada bloque asignado dinámicamente en la fuente y copiar todos los valores. Entonces necesitas desasignar toda esa memoria que acabas de copiar. Sin embargo, mover una gran parte vector<string>significa simplemente copiar algunos puntos (que se refieren al bloque de memoria dinámica) al destino y ponerlos en cero en la fuente.


En términos fáciles (prácticos):

Copiar un objeto significa copiar sus miembros "estáticos" y llamar al newoperador por sus objetos dinámicos. ¿Derecha?

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

Sin embargo, mover un objeto (repito, en un punto de vista práctico) implica solo copiar los punteros de objetos dinámicos, y no crear otros nuevos.

Pero, ¿eso no es peligroso? Por supuesto, podría destruir un objeto dinámico dos veces (falla de segmentación). Por lo tanto, para evitar eso, debe "invalidar" los punteros de origen para evitar destruirlos dos veces:

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

Ok, pero si muevo un objeto, el objeto fuente se vuelve inútil, ¿no? Por supuesto, pero en ciertas situaciones eso es muy útil. La más evidente es cuando llamo a una función con un objeto anónimo (temporal, objeto rvalue, ..., puede llamarlo con diferentes nombres):

void heavyFunction(HeavyType());

En esa situación, se crea un objeto anónimo, luego se copia en el parámetro de función y luego se elimina. Entonces, aquí es mejor mover el objeto, porque no necesita el objeto anónimo y puede ahorrar tiempo y memoria.

Esto lleva al concepto de una referencia "rvalue". Existen en C ++ 11 solo para detectar si el objeto recibido es anónimo o no. Creo que ya sabe que un "lvalue" es una entidad asignable (la parte izquierda del =operador), por lo que necesita una referencia con nombre a un objeto para poder actuar como un lvalue. Un valor de r es exactamente lo contrario, un objeto sin referencias con nombre. Por eso, objeto anónimo y rvalue son sinónimos. Asi que:

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

En este caso, cuando un objeto de tipo Adebe ser "copiado", el compilador crea una referencia de valor l o una referencia de valor de acuerdo con si el objeto pasado es nombrado o no. Cuando no, se llama a su constructor de movimientos y sabe que el objeto es temporal y puede mover sus objetos dinámicos en lugar de copiarlos, ahorrando espacio y memoria.

Es importante recordar que los objetos "estáticos" siempre se copian. No hay formas de "mover" un objeto estático (objeto en la pila y no en el montón). Por lo tanto, la distinción "mover" / "copiar" cuando un objeto no tiene miembros dinámicos (directa o indirectamente) es irrelevante.

Si su objeto es complejo y el destructor tiene otros efectos secundarios, como llamar a la función de una biblioteca, llamar a otras funciones globales o lo que sea, quizás sea mejor señalar un movimiento con una bandera:

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 */ } };

Por lo tanto, su código es más corto (no necesita hacer una nullptrasignación para cada miembro dinámico) y es más general.

Otra pregunta típica: ¿cuál es la diferencia entre A&&y const A&&? Por supuesto, en el primer caso, puede modificar el objeto y en el segundo no, pero ¿significado práctico? En el segundo caso, no puede modificarlo, por lo que no tiene formas de invalidar el objeto (excepto con una bandera mutable o algo así), y no hay una diferencia práctica en un constructor de copia.

¿Y qué es el reenvío perfecto ? Es importante saber que una "referencia rvalue" es una referencia a un objeto con nombre en el "ámbito de la persona que llama". Pero en el ámbito real, una referencia rvalue es un nombre para un objeto, por lo tanto, actúa como un objeto nombrado. Si pasa una referencia de valor a otra función, está pasando un objeto con nombre, por lo tanto, el objeto no se recibe como un objeto temporal.

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

El objeto ase copiaría al parámetro real de other_function. Si desea que el objeto acontinúe siendo tratado como un objeto temporal, debe usar la std::movefunción:

other_function(std::move(a));

Con esta línea, std::movese convertirá aa un valor de r y other_functionrecibirá el objeto como un objeto sin nombre. Por supuesto, si other_functionno tiene una sobrecarga específica para trabajar con objetos sin nombre, esta distinción no es importante.

¿Es ese reenvío perfecto? No, pero estamos muy unidos. El reenvío perfecto solo es útil para trabajar con plantillas, con el propósito de decir: si necesito pasar un objeto a otra función, necesito que si recibo un objeto con nombre, el objeto se pase como un objeto con nombre, y cuando no, Quiero pasarlo como un objeto sin nombre:

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

Esa es la firma de una función prototípica que utiliza un reenvío perfecto, implementada en C ++ 11 por medio de std::forward. Esta función explota algunas reglas de creación de instancias de plantillas:

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

Por lo tanto, si Tes una referencia de valor a A( T = A &), atambién ( A & && => A &). Si Tes una referencia de valor a A, atambién (A && &&& => A &&). En ambos casos, aes un objeto con nombre en el alcance real, pero Tcontiene la información de su "tipo de referencia" desde el punto de vista del alcance del llamante. Esta información ( T) se pasa como parámetro de plantilla a forwardy ''a'' se mueve o no según el tipo de T.


Es como copiar la semántica, pero en lugar de tener que duplicar todos los datos, puede robar los datos del objeto que se "mueve".


Estoy escribiendo esto para asegurarme de que lo entiendo correctamente.

Las semánticas de Move se crearon para evitar la copia innecesaria de objetos grandes. Bjarne Stroustrup en su libro "El lenguaje de programación C ++" usa dos ejemplos en los que se realiza una copia innecesaria por defecto: uno, el intercambio de dos objetos grandes y dos, la devolución de un objeto grande desde un método.

El intercambio de dos objetos grandes generalmente implica copiar el primer objeto en un objeto temporal, copiar el segundo objeto en el primer objeto y copiar el objeto temporal en el segundo objeto. Para un tipo incorporado, esto es muy rápido, pero para objetos grandes estas tres copias pueden llevar mucho tiempo. Una "asignación de movimiento" permite al programador anular el comportamiento de copia predeterminado y, en cambio, intercambiar referencias a los objetos, lo que significa que no hay ninguna copia y la operación de intercambio es mucho más rápida. La asignación de movimiento se puede invocar llamando al método std :: move ().

Devolver un objeto de un método por defecto implica hacer una copia del objeto local y sus datos asociados en una ubicación que sea accesible para la persona que llama (porque el objeto local no es accesible para la persona que llama y desaparece cuando finaliza el método). Cuando se devuelve un tipo incorporado, esta operación es muy rápida, pero si se devuelve un objeto grande, esto podría llevar mucho tiempo. El constructor de movimientos permite al programador anular este comportamiento predeterminado y, en cambio, "reutilizar" los datos de montón asociados con el objeto local al señalar el objeto que se devuelve al llamante para acumular los datos asociados con el objeto local. Por lo tanto, no se requiere copia.

En idiomas que no permiten la creación de objetos locales (es decir, objetos en la pila), estos tipos de problemas no se producen, ya que todos los objetos se asignan en el montón y siempre se accede a ellos por referencia.


¿Sabes lo que significa una copia semántica, verdad? significa que tiene tipos que se pueden copiar, para los tipos definidos por el usuario que define esto compre explícitamente escribiendo un constructor de copia y operador de asignación o el compilador los genera implícitamente. Esto hará una copia.

La semántica de movimiento es básicamente un tipo definido por el usuario con un constructor que toma una referencia de valor r (nuevo tipo de referencia que usa && (sí, dos símbolos)) que no es constante, esto se denomina constructor de movimiento, lo mismo ocurre con el operador de asignación. Entonces, ¿qué hace un constructor de movimientos? Bueno, en lugar de copiar la memoria del argumento de origen, "mueve" la memoria del origen al destino.

¿Cuándo querrías hacer eso? well std :: vector es un ejemplo, digamos que creó un std :: vector temporal y que lo devuelve desde una función diga:

std::vector<foo> get_foos();

Tendrá una sobrecarga del constructor de copia cuando la función regrese, si (y lo hará en C ++ 0x) std :: vector tiene un constructor de movimiento en lugar de copiar, solo puede configurar sus punteros y ''mover'' asignados dinámicamente Memoria a la nueva instancia. Es algo así como semántica de transferencia de propiedad con std :: auto_ptr.


Para ilustrar la necesidad de la semántica de movimiento , consideremos este ejemplo sin semántica de movimiento:

Aquí hay una función que toma un objeto de tipo Ty devuelve un objeto del mismo tipo T:

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

La función anterior utiliza la llamada por valor, lo que significa que cuando esta función se denomina, un objeto debe construirse para ser utilizado por la función.
Debido a que la función también devuelve por valor , otro objeto nuevo se construye para el valor de retorno:

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

Se han construido dos objetos nuevos, uno de los cuales es un objeto temporal que solo se utiliza durante la duración de la función.

Cuando el nuevo objeto se crea a partir del valor de retorno, se llama al constructor de copia para copiar el contenido del objeto temporal en el nuevo objeto b. Una vez que se completa la función, el objeto temporal utilizado en la función queda fuera del alcance y se destruye.

Ahora, vamos a considerar lo que hace un constructor de copia .

Primero debe inicializar el objeto, luego copiar todos los datos relevantes del objeto antiguo al nuevo.
Dependiendo de la clase, tal vez sea un contenedor con mucha información, entonces eso podría representar mucho tiempo y uso de memoria

// 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); }

Con la semántica de movimientos ahora es posible hacer que la mayor parte de este trabajo sea menos desagradable simplemente moviendo los datos en lugar de copiarlos.

// 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); }

Mover los datos implica volver a asociar los datos con el nuevo objeto. Y ninguna copia tiene lugar en absoluto.

Esto se logra con una rvaluereferencia.
Una rvaluereferencia funciona como una lvaluereferencia con una diferencia importante:
una referencia rvalue se puede mover y un lvalue no puede.

Desde cppreference.com :

Para hacer posible una fuerte excepción de excepción, los constructores de movimientos definidos por el usuario no deben lanzar excepciones. De hecho, los contenedores estándar generalmente dependen de std :: move_if_noexcept para elegir entre mover y copiar cuando los elementos del contenedor necesitan ser reubicados. Si se proporcionan los constructores de copia y movimiento, la resolución de sobrecarga selecciona el constructor de movimiento si el argumento es un valor r (ya sea un valor predeterminado como un temporal sin nombre o un valor x como el resultado de std :: move), y selecciona el constructor de copia si el argumento es un lvalue (objeto denominado o una función / operador que devuelve lvalue reference). Si solo se proporciona el constructor de copia, todas las categorías de argumentos lo seleccionan (siempre que tome una referencia a const, ya que los valores r pueden vincularse a las referencias de const), lo que hace que la copia del retroceso para el movimiento, cuando el movimiento no esté disponible. En muchas situaciones,Los constructores de movimientos se optimizan incluso si producen efectos secundarios observables, consulte el tema de copia. Un constructor se llama "constructor de movimiento" cuando toma una referencia rvalue como parámetro. No está obligado a mover nada, no se requiere que la clase tenga un recurso para ser movido y un ''constructor de movimiento'' no puede mover un recurso como en el caso permitido (pero tal vez no sensato) donde el parámetro es un Referencia constante del valor (const T &&).puede no ser capaz de mover un recurso como en el caso permitido (pero tal vez no sensible) donde el parámetro es una referencia de valor constante (const T &&).puede no ser capaz de mover un recurso como en el caso permitido (pero tal vez no sensible) donde el parámetro es una referencia de valor constante (const T &&).


Si está realmente interesado en una buena explicación en profundidad de la semántica de movimientos, le recomiendo que lea el documento original sobre ellos, "Una propuesta para agregar soporte de semánticas de movimientos al lenguaje C ++".

Es muy accesible y fácil de leer, y constituye un excelente caso para los beneficios que ofrecen. Hay otros artículos más recientes y actualizados sobre la semántica de movimientos disponibles en el sitio web de WG21 , pero este es probablemente el más sencillo, ya que se aproxima a las cosas desde una vista de nivel superior y no tiene mucho que ver con los detalles del lenguaje.