¿Qué es la semántica de movimiento?


1705

Acabo de terminar de escuchar la entrevista de 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 ahora estoy realmente entusiasmado con C ++ 0x, con la excepción de una. Todavía no obtengo semántica de movimiento ... ¿Qué es exactamente?


20
Encontré [el artículo del blog de Eli Bendersky] ( eli.thegreenplace.net/2011/12/15/… ) sobre valores y valores en C y C ++ bastante informativo. También menciona referencias de valor en C ++ 11 y las presenta con pequeños ejemplos.
Nils


19
Cada año más o menos me pregunto de qué se trata la "nueva" semántica de movimiento en C ++, lo busco en Google y llego a esta página. Leo las respuestas, mi cerebro se apaga. Vuelvo a C y me olvido de todo. Estoy estancado
cielo

77
@sky Considere std :: vector <> ... En algún lugar hay un puntero a una matriz en el montón. Si copia este objeto, debe asignarse un nuevo búfer y los datos del búfer deben copiarse en el nuevo búfer. ¿Hay alguna circunstancia en la que estaría bien simplemente robar el puntero? La respuesta es SÍ, cuando el compilador sabe que el objeto es temporal. La semántica de movimiento le permite definir cómo se pueden mover las tripas de sus clases y colocarlas en un objeto diferente cuando el compilador sabe que el objeto del que se está moviendo está a punto de desaparecer.
dicroce

La única referencia que puedo entender: learncpp.com/cpp-tutorial/… , es decir, el razonamiento original de la semántica de movimientos proviene de punteros inteligentes.
jw_

Respuestas:


2484

Me resulta más fácil entender la semántica de movimiento 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 <cstring>
#include <algorithm>

class string
{
    char* data;

public:

    string(const char* p)
    {
        size_t size = std::strlen(p) + 1;
        data = new char[size];
        std::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 = std::strlen(that.data) + 1;
        data = new char[size];
        std::memcpy(data, that.data, size);
    }

El constructor de copia define lo que significa copiar objetos de cadena. El parámetro se const string& thatune 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 idea clave de la semántica del movimiento. Tenga en cuenta que solo en la primera línea donde copiamos xes realmente necesaria esta copia profunda, porque es posible que deseemos inspeccionar xmás tarde y nos sorprendería mucho si xhubiera cambiado de alguna manera. ¿Te diste cuenta de cómo acabo de decir xtres veces (cuatro veces si incluyes esta oración) y quise decir exactamente el mismo objeto cada vez? Llamamos expresiones comox "valores".

Los argumentos en las líneas 2 y 3 no son valores, sino valores, 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 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 by c, podríamos hacer lo que quisiéramos con la cadena de origen, y el cliente no podría notar la diferencia !

C ++ 0x introduce un nuevo mecanismo llamado "referencia de valor" que, entre otras cosas, nos permite detectar argumentos de valor mediante 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 un estado válido:

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

Que hemos hecho aqui En lugar de copiar profundamente los datos del montón, acabamos de copiar el puntero y luego establecer el puntero original en nulo (para evitar que 'eliminar []' del destructor del objeto fuente libere nuestros 'datos recién robados'). En efecto, hemos "robado" los datos que originalmente pertenecían a la cadena de origen. Una vez más, la idea clave es que bajo ninguna circunstancia el cliente podría detectar que la fuente había sido modificada. Como realmente no hacemos una copia aquí, llamamos a este constructor un "constructor de movimientos". Su trabajo es mover recursos de un objeto a otro en lugar de copiarlos.

¡Felicitaciones, ahora comprende 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 e intercambiar , apréndalo y regrese, porque es un idioma increíble de C ++ relacionado con la seguridad de excepciones.

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

Huh, eso es todo? "¿Dónde está la referencia de valor?" usted puede preguntar "¡No lo necesitamos aquí!" es mi respuesta :)

Tenga en cuenta que pasamos el parámetro that por valor , por lo thatque debe inicializarse como cualquier otro objeto de cadena. ¿Exactamente cómo se thatva a inicializar? En los viejos tiempos de C ++ 98 , la respuesta habría sido "por el constructor de la copia". En C ++ 0x, el compilador 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 usted dice a = b, el constructor de la copia se inicializará that(porque la expresión bes 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 idioma de copiar e intercambiar: hacer una copia, intercambiar el contenido con la copia y luego deshacerse de la copia dejando el alcance. Nada nuevo aquí.

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

Para resumir, el constructor de copia realiza una copia profunda, porque la fuente debe permanecer intacta. El constructor de movimiento, 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 entendido el punto principal. Hay mucho más para valorar las referencias y mover la semántica que dejé intencionalmente para simplificar. Si desea más detalles, consulte mi respuesta complementaria .


40
@ Pero si mi ctor está obteniendo un valor r, que nunca puede usarse más tarde, ¿por qué tengo que molestarme en dejarlo en un estado constante / seguro? En lugar de establecer that.data = 0, ¿por qué no dejarlo en paz?
einpoklum

70
@einpoklum Porque sin that.data = 0, los personajes serían destruidos demasiado pronto (cuando el temporal muere), y también dos veces. ¡Desea robar los datos, no compartirlos!
fredoverflow

19
@einpoklum El destructor programado regularmente todavía se ejecuta, por lo que debe asegurarse de que el estado posterior al movimiento del objeto de origen no cause un bloqueo. Mejor, debe asegurarse de que el objeto fuente también pueda ser el receptor de una tarea u otra escritura.
CTMacUser

12
@pranitkothari Sí, todos los objetos deben ser destruidos, incluso los objetos movidos. Y dado que no queremos que la matriz de caracteres se elimine cuando eso sucede, tenemos que establecer el puntero en nulo.
fredoverflow

77
@ Virus721 delete[]en un nullptr está definido por el estándar C ++ como no operativo.
fredoverflow

1058

Mi primera respuesta fue una introducción extremadamente simplificada para mover la semántica, y se omitieron muchos detalles a propósito para que fuera 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 me pareció correcto 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 comentarios valiosos. Muchas gracias, Stephan!

Introducción

La semántica de movimiento permite que un objeto, bajo ciertas condiciones, tome posesión de los recursos externos de otro objeto. Esto es importante de dos maneras:

  1. Convirtiendo 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), 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. Implementación de tipos seguros de "solo movimiento"; es decir, los tipos para los que copiar no tiene sentido, pero el movimiento sí. Los ejemplos incluyen bloqueos, identificadores de archivos e indicadores inteligentes con una semántica de propiedad única. Nota: Esta respuesta trata sobre std::auto_ptruna plantilla de biblioteca estándar C ++ 98 en desuso, que fue reemplazada porstd::unique_ptr en C ++ 11. Los programadores intermedios de C ++ probablemente estén al menos algo familiarizados std::auto_ptr, y debido a la "semántica de movimiento" que muestra, parece un buen punto de partida para discutir la semántica de movimiento en C ++ 11. YMMV.

¿Qué es un movimiento?

La biblioteca estándar C ++ 98 ofrece un puntero inteligente con una semántica de propiedad única llamada std::auto_ptr<T>. En caso de que no esté familiarizado auto_ptr, su propósito es garantizar que siempre se libere un objeto asignado dinámicamente, 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 auto_ptres 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 | | | |
  |   +---+ |            |   +---+ |
  +---------+            +---------+

Nota cómo la inicialización de bcon aqué no copiar el triángulo, sino que transfiere la propiedad del triángulo de aa b. También decimos " ase mueve a b " o "el triángulo se mueve de a 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 un recurso que administra a otro objeto.

El constructor de copia de auto_ptrprobablemente 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 esto auto_ptres que lo que parece sintácticamente una copia es en realidad un movimiento. Intentar llamar a una función miembro en un auto_ptrtraspaso invocará un comportamiento indefinido, por lo que debe tener mucho cuidado de no usar un auto_ptrdespués de que se haya movido desde:

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_ptrno siempre es peligroso. Las funciones de fábrica son un caso de uso perfecto 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

Observe 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. Entonces, ¿cuál es la diferencia entre las expresiones ay make_triangle()? ¿No son los dos del mismo tipo? De hecho lo son, pero tienen diferentes categorías de valor .

Categorías de valor

Obviamente, debe haber alguna diferencia profunda entre la expresión aque denota una auto_ptrvariable y la expresión make_triangle()que denota la llamada de una función que devuelve un auto_ptrby, creando así un nuevo auto_ptrobjeto temporal cada vez que se llama. aes un ejemplo de un lvalue , mientras que make_triangle()es un ejemplo de un rvalue .

Pasar de valores como aes peligroso, ya que más tarde podríamos intentar llamar a una función miembro ainvocando un comportamiento indefinido. Por otro lado, pasar de valores como make_triangle()es perfectamente seguro, porque después de que el constructor de copia haya hecho su trabajo, no podemos usar el temporal nuevamente. No hay expresión que denote dicho temporal; Si simplemente escribimos de make_triangle()nuevo, obtenemos un temporal diferente . De hecho, el traslado temporal ya no está en la siguiente línea:

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

Tenga en cuenta que las letras lyr 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 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 alcance denota el mismo objeto temporal.

Referencias de valor

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

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

Si nos adentramos consten la mezcla, ya tenemos cuatro tipos diferentes de referencias. ¿A qué tipos de expresiones de tipo se Xpueden unir?

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

En la práctica, te puedes olvidar const X&&. Estar restringido para leer valores no es muy útil.

Una referencia de valor X&&es un nuevo tipo de referencia que solo se une a los valores.

Conversiones implícitas

Las referencias de Rvalue pasaron por varias versiones. Desde la versión 2.1, una referencia de valor X&&también se une a todas las categorías de valores de un tipo diferente Y, siempre que haya una conversión implícita de Ya X. En ese caso, Xse crea un tipo temporal , 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 l const char[12]. Dado que no hay una conversión implícita de const char[12]a través const char*de std::string, un temporal de tipo std::stringse crea, yr está obligado a que temporal. Este es uno de los casos en los que la distinción entre valores (expresiones) y temporales (objetos) es un poco borrosa.

Mover constructores

Un ejemplo útil de una función con un X&&parámetro es el constructor de movimiento 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>ha sido reemplazado por el std::unique_ptr<T>que aprovecha las referencias rvalue. Desarrollaré y discutiré una versión simplificada de unique_ptr. Primero, encapsulamos un puntero sin formato y sobrecargamos los operadores ->y *, por lo tanto, 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 movimiento:

    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 auto_ptrconstructor de copias, 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 aes un valor l, pero el parámetro unique_ptr&& sourcesolo puede vincularse a valores. Esto es exactamente lo que queríamos; movimientos peligrosos nunca deben ser implícitos. La tercera línea compila muy bien, porque make_triangle()es un valor r. El movimiento constructor transferirá la propiedad de la que temporal c. De nuevo, 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 que falta es el operador de asignación de movimiento. Su trabajo es liberar el antiguo recurso 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. ¿Conoces el idioma de copiar y cambiar? También se puede aplicar para mover la semántica como el modismo de mover y cambiar:

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

Ahora que sourcees una variable de tipo unique_ptr, será inicializada por el constructor de movimiento; es decir, el argumento se moverá al parámetro. Todavía se requiere que el argumento sea un valor r, porque el constructor de movimiento tiene un parámetro de referencia rvalue. Cuando el flujo de control alcanza la llave de cierre operator=, sourcese sale del alcance y libera 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 modismo move-and-swap simplifica la implementación.

Mudarse de 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 movimiento, aunque pueda ser potencialmente inseguro. Para este propósito, C ++ 11 ofrece una plantilla de función de biblioteca estándar llamada std::movedentro del encabezado <utility>. Este nombre es un poco desafortunado, porque std::movesimplemente arroja un valor l a un valor r; sí no se mueve nada por sí misma. Simplemente permite moverse. Tal vez debería haber sido nombrado std::cast_to_rvalueo std::enable_move, pero ahora estamos atrapados con el nombre.

Así es como se mueve explícitamente desde 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, aya no posee un triángulo. Está bien, porque al escribir explícitamentestd::move(a) , dejamos en claro nuestras intenciones: "Estimado constructor, haz lo que quieras apara inicializar c; ya no me importa a. Siéntete libre de seguir tu camino a".

std::move(some_lvalue) lanza un valor l a un valor r, permitiendo así un movimiento posterior.

Xvalores

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 se puede unir a una referencia de valor de lado derecho, a pesar de que no es un valor de lado derecho en el sentido tradicional, se denomina xValue (valor que expira). Los valores tradicionales fueron renombrados como valores ( valores puros).

Tanto los valores como los valores x son valores. Los valores X y los valores son ambos valores (valores generalizados). Las relaciones son más fáciles de comprender con un diagrama:

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

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

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

Salir de funciones

Hasta ahora, hemos visto movimientos en variables locales y en parámetros de función. Pero moverse también es posible en la dirección opuesta. Si una función regresa 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 returndeclaración como 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 se pueden mover implícitamente fuera de las funciones:

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

¿Cómo es que el constructor de movimientos acepta el valor l resultcomo argumento? El alcance de resultestá a punto de finalizar, y se destruirá durante el desbobinado de la pila. Nadie podría quejarse después de que eso resulthubiera cambiado de alguna manera; cuando el flujo de control vuelve a la persona que llama, ¡ resultya 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::movepara mover objetos automáticos fuera de las funciones, ya que esto inhibe la "optimización del valor de retorno con nombre" (NRVO).

Nunca use std::movepara mover objetos automáticos fuera de las 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 Rvalue siguen siendo referencias y, como siempre, nunca debe devolver una referencia a un objeto automático; la persona que llama terminaría con una referencia pendiente si engañaste al compilador para que acepte tu código, así:

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 del movimiento, no por std::move, y no simplemente vinculando un valor r a una referencia de valor r.

Pasando a miembros

Tarde o temprano, vas a escribir 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 parameteres un valor. Si observa su tipo, verá una referencia de valor r, pero una referencia de valor simplemente significa "una referencia vinculada a un valor r"; ¡ no significa que la referencia en sí misma sea un valor! De hecho, parameteres solo una variable ordinaria con un nombre. Puede usar parametertantas veces como desee dentro del cuerpo del constructor, y siempre denota el mismo objeto. Irse de manera implícita sería peligroso, de ahí que el lenguaje lo prohíba.

Una referencia de rvalue con nombre es un lvalue, como 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
    {}
};

Se podría argumentar que parameterya no se usa después de la inicialización de member. ¿Por qué no hay una regla especial para insertar silenciosamentestd::move como con los valores de retorno? Probablemente porque sería una carga excesiva 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 del valor de retorno simplemente tiene que verificar las tablas de símbolos para determinar si el identificador después de que la returnpalabra clave denota un objeto automático.

También puede pasar el parameterpor valor. Para tipos de solo movimiento comounique_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 Rvalue 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 se ajustan a la versión 3.0 todavía, por lo que deberá 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 movimientos u operador de asignación de movimientos, ni el constructor de copias ni el operador de asignación de copias se declararán implícitamente.

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

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

Tenga en cuenta que el operador de asignación de copia y el operador de asignación de movimiento pueden fusionarse en un único operador de asignación unificado, 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, el número de funciones miembro especiales para implementar cae de cinco a cuatro. Aquí hay una compensación entre seguridad de excepción y 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<typename T>
void foo(T&&);

Puede esperar T&&que solo se una a los valores, porque a primera vista, parece una referencia de valor. Sin embargo, resulta que T&&también se une a los valores:

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, Tse deduce que es X, por T&&lo tanto, significa X&&. Esto es lo que cualquiera esperaría. Pero si el argumento es un valor de tipo X, debido a una regla especial, Tse deduce que es X&, por T&&lo tanto , significaría algo así X& &&. Pero puesto que C ++ todavía no tiene 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 de valor, sino una referencia de reenvío. También se une a los valores, en cuyo caso Ty T&&son referencias de ambos valores.

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 comprende el colapso de referencias, así es como std::movese implementa:

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, moveacepta cualquier tipo de parámetro gracias a la referencia de reenvío T&&y devuelve una referencia de valor. La std::remove_reference<T>::typellamada a la metafunción es necesaria porque de lo contrario, para valores de tipo X, el tipo de retorno sería X& &&, que colapsaría X&. Como tsiempre es un valor de l (recuerde que una referencia de valor de r con nombre es un valor de l), pero queremos vincularnos ta una referencia de valor de r, tenemos que convertir explícitamente tal tipo de retorno correcto. La llamada de una función que devuelve una referencia rvalue es en sí misma un xvalue. Ahora ya sabes de dónde vienen los valores x;)

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

Tenga en cuenta que la devolución por referencia rvalue está bien en este ejemplo, porque tno denota un objeto automático, sino un objeto que fue pasado por la persona que llama.



24
Hay una tercera razón por la cual la semántica de movimiento es importante: seguridad de excepción. A menudo, cuando una operación de copia puede arrojar (porque necesita asignar recursos y la asignación puede fallar), una operación de movimiento puede ser no lanzar (porque puede transferir la propiedad de los recursos existentes en lugar de asignar recursos nuevos). Tener operaciones que no pueden fallar siempre es bueno, y puede ser crucial al escribir código que proporcione garantías de excepción.
Brangdon

8
Estuve contigo hasta las 'referencias universales', pero luego todo es demasiado abstracto para seguir. ¿Colapso de referencia? Reenvío perfecto? ¿Está diciendo que una referencia rvalue se convierte en una referencia universal si el tipo tiene una plantilla? ¡Desearía que hubiera una manera de explicar esto para saber si necesito entenderlo o no! :)
Kylotan

8
Escribe un libro ahora ... esta respuesta me ha dado razones para creer que si cubriste otras esquinas de C ++ de una manera lúcida como esta, miles de personas más lo entenderán.
halivingston

12
@halivingston Muchas gracias por sus amables comentarios, realmente lo aprecio. El problema con escribir un libro es: es mucho más trabajo de lo que puedas imaginar. Si desea profundizar en C ++ 11 y más allá, le sugiero que compre "Effective Modern C ++" de Scott Meyers.
fredoverflow

77

La semántica de movimiento se basa en referencias de valor .
Un valor r es un objeto temporal que se destruirá al final de la expresión. En C ++ actual, los valores solo se unen a constreferencias. C ++ 1x permitirá constreferencias no de valor, deletreadas T&&, que son referencias a objetos de valor de valor.
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 a é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 viejos compiladores el resultado de f()se copian en xel uso de X's constructor de copia. Si su compilador admite la semántica de movimiento y Xtiene un constructor de movimiento, se llama en su lugar. Dado que su rhsargumento es un valor r , sabemos que ya no es necesario y podemos robarle 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 en un vacío X, se mueven al temporal, que se destruirá después de la asignación).


1
tenga en cuenta que debería ser this->swap(std::move(rhs));porque las referencias de rvalue nombradas son valores
wmamrak

Esto es un poco mal, por el comentario de @ Tacyt: rhses un valor-I en el contexto de X::X(X&& rhs). std::move(rhs)Debe llamar para obtener un valor r, pero esto hace que la respuesta sea discutible.
Asherah

¿Qué significa la semántica de movimiento para tipos sin punteros? Mover la semántica funciona copia parecida?
Gusev Slava

@Gusev: No tengo idea de lo que estás preguntando.
sbi

60

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 compilador de C ++ ordinario creará un objeto temporal para el resultado de multiply(), llamará al constructor de copia para que se inicialice 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 que se inicie rcopiando su contenido y luego descarte el valor temporal sin tener que destruirlo.

Esto es especialmente importante si (como quizás el Matrixejemplo anterior), el objeto que se copia 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 usar el conteo de referencias y la semántica de copia en escritura de manera interna. Un constructor de movimientos dejaría sola la memoria de almacenamiento dinámico y simplemente copiaría el puntero dentro del Matrixobjeto.


2
¿En qué se diferencian los constructores de movimiento y los constructores de copia?
dicroce

1
@dicroce: Se diferencian por la sintaxis, uno se parece a Matrix (const Matrix & src) (constructor de copias) y el otro se parece a Matrix (Matrix && src) (mover constructor), consulte mi respuesta principal para ver un mejor ejemplo.
snk_kid

3
@dicroce: Uno hace un objeto en blanco y otro hace una copia. Si los datos almacenados en el objeto son grandes, una copia puede ser costosa. Por ejemplo, std :: vector.
Billy ONeal

1
@ kunj2aan: Sospecho que depende de tu compilador. El compilador podría crear un objeto temporal dentro de la función y luego moverlo al valor de retorno de la persona que llama. O bien, puede construir directamente el objeto en el valor de retorno, sin necesidad de usar un constructor de movimiento.
Greg Hewgill

2
@Jichao: Esa es una optimización llamada RVO, vea esta pregunta para obtener más información sobre la diferencia: stackoverflow.com/questions/5031778/…
Greg Hewgill

30

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

Es muy accesible y fácil de leer y es un excelente caso para los beneficios que ofrecen. Hay otros documentos más recientes y actualizados sobre la semántica de movimientos disponibles en el sitio web WG21 , pero este es probablemente el más sencillo, ya que aborda las cosas desde una vista de nivel superior y no entra mucho en los detalles del lenguaje arenoso.


27

La semántica de movimiento se trata de transferir recursos en lugar de copiarlos cuando ya nadie necesita el valor fuente.

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 de una función, a menos que RVO entre en acción, el valor que está devolviendo se copia en el marco de la pila de la persona que llama, y ​​luego se sale del alcance y se destruye. Este es solo uno de muchos ejemplos: vea el paso por valor cuando el objeto fuente es temporal, los algoritmos como sortese simplemente reorganizan los elementos, la reasignación vectorcuando capacity()se excede, etc.

Cuando tales pares de copia / destrucción son caros, generalmente se debe a que el objeto posee algún recurso 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 nueva memoria para cada bloque asignado dinámicamente en el origen y copiar todos los valores. Entonces necesita desasignar toda esa memoria que acaba de copiar. Sin embargo, mover un archivo grande vector<string>significa simplemente copiar algunos punteros (que se refieren al bloque de memoria dinámica) al destino y ponerlos a cero en la fuente.


23

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, desde un punto de vista práctico) implica solo copiar los punteros de los objetos dinámicos, y no crear nuevos.

Pero, ¿no es eso peligroso? Por supuesto, podría destruir un objeto dinámico dos veces (falla de segmentación). Entonces, 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 invoco 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 al 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 de "valor". 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 r es exactamente lo contrario, un objeto sin referencias nombradas. Por eso, objeto anónimo y rvalue son sinónimos. Entonces:

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 o una referencia de valor de acuerdo a si el objeto pasado es nombrado o no. Cuando no, se llama a su constructor de movimiento 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 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 para un constructor de copias.

¿Y qué es el reenvío perfecto ? Es importante saber que una "referencia de valor" es una referencia a un objeto nombrado en el "alcance del llamante". Pero en el ámbito real, una referencia rvalue es un nombre para un objeto, por lo tanto, actúa como un objeto con nombre. Si pasa una referencia de valor r 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á 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, se std::movelanzará aa un valor 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 cerca. 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 el reenvío perfecto, implementado en C ++ 11 por medio de std::forward. Esta función explota algunas reglas de creación de instancias de plantilla:

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

Entonces, si Tes una referencia de valor a A( T = A &), atambién ( A & && => A &). Si Tes una referencia de valor 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 forwardy 'a' se mueve o no según el tipo de T.


20

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


13

Sabes lo que significa una semántica de copia, ¿verdad? significa que tiene tipos que se pueden copiar, para los tipos definidos por el usuario, defina esto ya sea comprando explícitamente escribiendo un constructor de copia y un 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 usando && (sí, dos símbolos)) que no es constante, esto se llama un constructor de movimiento, lo mismo ocurre con el operador de asignación. Entonces, ¿qué hace un constructor de movimiento? Bueno, en lugar de copiar la memoria de su argumento de origen, 'mueve' la memoria del origen al destino.

¿Cuándo quieres hacer eso? Bien, std :: vector es un ejemplo, digamos que creó un std :: vector temporal y lo devuelve desde una función que dice:

std::vector<foo> get_foos();

Tendrá gastos generales 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 copiarlo, solo puede configurar sus punteros y 'mover' dinámicamente asignado memoria a la nueva instancia. Es algo así como la semántica de transferencia de propiedad con std :: auto_ptr.


1
No creo que este sea un gran ejemplo, porque en estos ejemplos de valores de retorno de funciones, la Optimización del valor de retorno probablemente ya esté eliminando la operación de copia.
Zan Lynx

7

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 se llama a esta función, se debe construir un objeto para ser utilizado por la función.
Como la función también regresa por valor , se construye otro nuevo objeto para el valor de retorno:

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

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

Cuando se crea el nuevo objeto 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 de alcance y se destruye.


Ahora, consideremos lo que hace un constructor de copias .

Primero debe inicializar el objeto, luego copiar todos los datos relevantes del objeto antiguo al nuevo.
Dependiendo de la clase, quizás sea un contenedor con muchos datos, 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 movimiento 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 más o menos como una lvaluereferencia con una diferencia importante:
una referencia rvalue se puede mover y un lvalue no.

Desde cppreference.com :

Para hacer posible una fuerte garantía de excepción, los constructores de movimiento definidos por el usuario no deben lanzar excepciones. De hecho, los contenedores estándar generalmente se basan en 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 (un valor pr tal 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 nombrado o una función / operador que devuelve la referencia de lvalue). 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 pueden unirse a las referencias de const), lo que hace que la copia de la reserva para el movimiento, cuando el movimiento no esté disponible. En muchas situaciones, los constructores de movimiento se optimizan incluso si produjeran efectos secundarios observables, ver copia de elisión. Un constructor se llama 'constructor de movimiento' cuando toma una referencia de valor como parámetro. No está obligado a mover nada, no se requiere que la clase tenga un recurso para mover y un 'constructor de movimiento' puede no ser capaz de mover un recurso como en el caso permitido (pero quizás no sensible) donde el parámetro es un const rvalue reference (const T &&).


7

Estoy escribiendo esto para asegurarme de que lo entiendo correctamente.

La semántica de movimiento se creó para evitar la copia innecesaria de objetos grandes. Bjarne Stroustrup en su libro "El lenguaje de programación C ++" utiliza dos ejemplos en los que se producen copias innecesarias por defecto: uno, el intercambio de dos objetos grandes y dos, la devolución de un objeto grande de un método.

Intercambiar dos objetos grandes generalmente implica copiar el primer objeto a un objeto temporal, copiar el segundo objeto al primer objeto y copiar el objeto temporal al segundo objeto. Para un tipo integrado, esto es muy rápido, pero para objetos grandes estas tres copias pueden llevar una gran cantidad de 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 movimiento permite al programador anular este comportamiento predeterminado y, en su lugar, "reutilizar" los datos de almacenamiento dinámico asociados con el objeto local al señalar el objeto que se devuelve al llamante para acumular datos asociados con el objeto local. Por lo tanto, no se requiere copia.

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


"Una" asignación de movimiento "permite al programador anular el comportamiento de copia predeterminado y, en su lugar, intercambiar referencias a los objetos, lo que significa que no hay copia y la operación de intercambio es mucho más rápida". - Estas afirmaciones son ambiguas y engañosas. Para intercambiar dos objetos xy y, no puede simplemente "intercambiar referencias a los objetos" ; puede ser que los objetos contengan punteros que hacen referencia a otros datos, y esos punteros pueden intercambiarse, pero los operadores de movimiento no están obligados a intercambiar nada. Pueden borrar los datos del objeto desde el cual se mueven, en lugar de preservar los datos de destino en el mismo.
Tony Delroy

Podrías escribir swap()sin mover la semántica. "La asignación de movimiento se puede invocar llamando al método std :: move ()". - a veces es necesario usarlo std::move(), aunque en realidad eso no mueve nada, solo le permite al compilador saber que el argumento es movible, a veces std::forward<>()(con referencias de reenvío), y otras veces el compilador sabe que un valor se puede mover.
Tony Delroy

-2

Aquí hay una respuesta del libro "El lenguaje de programación C ++" de Bjarne Stroustrup. Si no desea ver el video, puede ver el texto a continuación:

Considera este fragmento. Volver de un operador + implica copiar el resultado fuera de la variable local resy en un lugar donde la persona que llama pueda acceder a él.

Vector operator+(const Vector& a, const Vector& b)
{
    if (a.size()!=b.size())
        throw Vector_siz e_mismatch{};
    Vector res(a.size());
        for (int i=0; i!=a.size(); ++i)
            res[i]=a[i]+b[i];
    return res;
}

Realmente no queríamos una copia; solo queríamos obtener el resultado de una función. Por lo tanto, debemos mover un Vector en lugar de copiarlo. Podemos definir el constructor de movimientos de la siguiente manera:

class Vector {
    // ...
    Vector(const Vector& a); // copy constructor
    Vector& operator=(const Vector& a); // copy assignment
    Vector(Vector&& a); // move constructor
    Vector& operator=(Vector&& a); // move assignment
};

Vector::Vector(Vector&& a)
    :elem{a.elem}, // "grab the elements" from a
    sz{a.sz}
{
    a.elem = nullptr; // now a has no elements
    a.sz = 0;
}

El && significa "referencia de valor r" y es una referencia a la que podemos vincular un valor r. "rvalue" 'está destinado a complementar "lvalue", que en términos generales significa "algo que puede aparecer en el lado izquierdo de una tarea". Por lo tanto, un valor r significa aproximadamente "un valor al que no se puede asignar", como un entero devuelto por una llamada a la función y la resvariable local en el operador + () para Vectores.

¡Ahora, la declaración return res;no se copiará!

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.