¿Qué es std :: move (), y cuándo debería usarse?

656
  1. ¿Qué es?
  2. ¿Qué hace?
  3. ¿Cuándo debería usarse?

Se agradecen los buenos enlaces.

Basilevs
fuente
43
Bjarne Stroustrup explica el movimiento en Una breve introducción a las referencias de
Rvalue
1
Mover semántica
Basilevs
12
Esta pregunta se refiere a std::move(T && t); También existe std::move(InputIt first, InputIt last, OutputIt d_first)un algoritmo relacionado con std::copy. Lo señalo para que otros no estén tan confundidos como yo cuando me enfrenté a std::movetomar tres argumentos. es.cppreference.com/w/cpp/algorithm/move
josaphatv

Respuestas:

287

Página de Wikipedia sobre referencias de valor R de C ++ 11 y constructores de movimiento

  1. En C ++ 11, además de copiar constructores, los objetos pueden tener constructores de movimiento.
    (Y además de copiar operadores de asignación, tienen operadores de asignación de movimiento).
  2. El constructor de movimiento se usa en lugar del constructor de copia, si el objeto tiene el tipo "rvalue-reference" (Type && ).
  3. std::move() es un modelo que produce una referencia de valor r a un objeto, para permitir moverse desde él.

Es una nueva forma de C ++ para evitar copias. Por ejemplo, usando un constructor de movimiento, a std::vectorpodría simplemente copiar su puntero interno a los datos al nuevo objeto, dejando el objeto movido en un estado movido desde, por lo tanto, no copiando todos los datos. Esto sería C ++ - válido.

Intente buscar en Google la semántica del movimiento, el valor, el reenvío perfecto.

Scharron
fuente
40
La semántica de movimiento requiere que el objeto movido siga siendo válido , lo que no es un estado incorrecto. (Justificación: todavía tiene que destruir, hacer que funcione.)
GManNickG
13
@GMan: bueno, tiene que estar en un estado que sea seguro de destruir, pero, AFAIK, no tiene que ser utilizable para nada más.
Zan Lynx
8
@ZanLynx: Correcto. Tenga en cuenta que la biblioteca estándar requiere adicionalmente que los objetos movidos sean asignables, pero esto es solo para objetos utilizados en stdlib, no un requisito general.
GManNickG
25
-1 "std :: move () es la forma en C ++ 11 de usar la semántica de movimiento" . std::move()no es la forma de usar la semántica de movimiento, la semántica de movimiento se realiza de forma transparente para el programador. movees solo un lanzamiento para pasar un valor de un punto a otro donde el valor original ya no se usará.
Manu343726
15
Yo iría más lejos std::moveen sí no hace "nada", tiene cero efectos secundarios. Simplemente le indica al compilador que al programador ya no le importa lo que le pase a ese objeto. es decir, se da permiso a otras partes del software para moverse desde el objeto, pero no requiere que se mueva. De hecho, el destinatario de una referencia de valor no tiene que hacer ninguna promesa sobre lo que hará o no con los datos.
Aaron McDaid
241

1. "¿Qué es?"

Si bien std::move() técnicamente es una función, diría que no es realmente una función . Es una especie de convertidor entre las formas en que el compilador considera el valor de una expresión.

2. "¿Qué hace?"

Lo primero a tener en cuenta es que std::move() realidad no mueve nada . Convierte una expresión de ser un valor l (como una variable con nombre) a ser un valor x . Un xvalue le dice al compilador:

Puedes saquearme, mover todo lo que tengo y usarlo en otro lugar (ya que de todos modos voy a ser destruido pronto) ".

en otras palabras, cuando usas std::move(x), estás permitiendo que el compilador canibalice x. Por lo tanto, si xtiene, por ejemplo, su propio búfer en la memoria, después destd::move() de que el compilador puede tener otro objeto propio.

También puedes moverte desde un valor priva (como un temporal que estás pasando), pero esto rara vez es útil.

3. "¿Cuándo se debe usar?"

Otra forma de hacer esta pregunta es "¿Para qué canibalizaría los recursos de un objeto existente?" bueno, si estás escribiendo código de aplicación, probablemente no estarás jugando mucho con objetos temporales creados por el compilador. Por lo tanto, principalmente haría esto en lugares como constructores, métodos de operador, funciones similares a algoritmos de biblioteca estándar, etc., donde los objetos se crean y destruyen mucho de forma automática. Por supuesto, eso es solo una regla general.

Un uso típico es 'mover' recursos de un objeto a otro en lugar de copiarlos. @Guillaume enlaza a esta página que tiene un ejemplo breve y sencillo: intercambiar dos objetos con menos copia.

template <class T>
swap(T& a, T& b) {
    T tmp(a);   // we now have two copies of a
    a = b;      // we now have two copies of b (+ discarded a copy of a)
    b = tmp;    // we now have two copies of tmp (+ discarded a copy of b)
}

El uso de mover le permite intercambiar los recursos en lugar de copiarlos:

template <class T>
swap(T& a, T& b) {
    T tmp(std::move(a));
    a = std::move(b);   
    b = std::move(tmp);
}

Piense en lo que sucede cuando Tes, por ejemplo, vector<int>de tamaño n. En la primera versión, lee y escribe 3 * n elementos, en la segunda versión básicamente lee y escribe solo los 3 punteros en los buffers de los vectores, más los tamaños de los 3 buffers. Por supuesto, la clase Tnecesita saber cómo moverse; su clase debe tener un operador de asignación de movimiento y un constructor de movimiento para la clase Tpara que esto funcione.

einpoklum
fuente
3
Durante mucho tiempo he oído hablar de estas semánticas de movimiento, nunca las examiné. A partir de esta descripción que ha dado, parece que es una copia superficial en lugar de una copia profunda.
Pez cebra
77
@TitoneMaurice: Excepto que no es una copia, ya que el valor original ya no es utilizable.
einpoklum
3
@ Zebrafish no podrías estar más equivocado. Una copia superficial deja el original exactamente en el mismo estado, un movimiento generalmente da como resultado que el original esté vacío o en un estado válido.
rubenvb
17
@rubenvb Zebra no está completamente equivocado. Si bien es cierto que el objeto cannabilizado original generalmente se sabotea deliberadamente para evitar errores confusos (por ejemplo, establecer sus punteros en nullptr para indicar que ya no posee los punteros), el hecho de que todo el movimiento se implementa simplemente copiando un puntero de la fuente al destino (y evitando deliberadamente hacer algo con el puntero) es una reminiscencia de una copia superficial. De hecho, iría tan lejos como para decir que un movimiento es una copia superficial, seguida opcionalmente por una autodestrucción parcial de la fuente. (cont.)
ligereza corre en órbita el
3
(cont.) Si permitimos esta definición (y me gusta bastante), entonces la observación de @ Zebrafish no es incorrecta, solo un poco incompleta.
Carreras de ligereza en órbita el
146

Puede usar move cuando necesite "transferir" el contenido de un objeto a otro lugar, sin hacer una copia (es decir, el contenido no está duplicado, por eso podría usarse en algunos objetos no copiables, como un unique_ptr). También es posible que un objeto tome el contenido de un objeto temporal sin hacer una copia (y ahorre mucho tiempo), con std :: move.

Este enlace realmente me ayudó:

http://thbecker.net/articles/rvalue_references/section_01.html

Lo siento si mi respuesta llega demasiado tarde, pero también estaba buscando un buen enlace para std :: move, y encontré los enlaces de arriba un poco "austeros".

Esto pone el énfasis en la referencia del valor r, en qué contexto debe usarlos, y creo que es más detallado, por eso quería compartir este enlace aquí.

Guillaume
fuente
26
Buen enlace Siempre encontré el artículo de Wikipedia y otros enlaces con los que me encontré bastante confusos, ya que simplemente te arrojan datos, dejándote a ti descubrir cuál es el significado / razón real. Si bien "mover semántica" en un constructor es bastante obvio, todos esos detalles sobre pasar && - los valores no lo son ... por lo que la descripción de estilo tutorial fue muy agradable.
Christian Stieber
66

Q: que es std::move ?

UNA: std::move() es una función de la Biblioteca estándar de C ++ para convertir a una referencia de valor r.

Simplísticamente std::move(t)es equivalente a:

static_cast<T&&>(t);

Un valor r es un temporal que no persiste más allá de la expresión que lo define, como un resultado de función intermedia que nunca se almacena en una variable.

int a = 3; // 3 is a rvalue, does not exist after expression is evaluated
int b = a; // a is a lvalue, keeps existing after expression is evaluated

Se proporciona una implementación para std :: move () en N2027: "Una breve introducción a las referencias de Rvalue" de la siguiente manera:

template <class T>
typename remove_reference<T>::type&&
std::move(T&& a)
{
    return a;
}

Como puede ver, std::movedevuelve T&&sin importar si se llama con un valor ( T), tipo de referencia ( T&) o rvalue reference ( T&&).

P: ¿Qué hace?

A: Como un elenco, no hace nada durante el tiempo de ejecución. Solo es relevante en el momento de la compilación decirle al compilador que le gustaría continuar considerando la referencia como un valor.

foo(3 * 5); // obviously, you are calling foo with a temporary (rvalue)

int a = 3 * 5;
foo(a);     // how to tell the compiler to treat `a` as an rvalue?
foo(std::move(a)); // will call `foo(int&& a)` rather than `foo(int a)` or `foo(int& a)`

Lo que no hace:

  • Haga una copia del argumento.
  • Llame al constructor de la copia.
  • Cambiar el objeto de argumento

P: ¿Cuándo se debe usar?

R: Debe usarlo std::movesi desea llamar a funciones que admiten la semántica de movimiento con un argumento que no es un valor r (expresión temporal).

Esto me plantea las siguientes preguntas de seguimiento:

  • ¿Qué son las semánticas de movimiento? La semántica de movimiento en contraste con la semántica de copia es una técnica de programación en la que los miembros de un objeto se inicializan 'tomando el control' en lugar de copiar los miembros de otro objeto. Tal 'toma de control' solo tiene sentido con punteros y controladores de recursos, que pueden transferirse de forma económica copiando el puntero o el controlador de enteros en lugar de los datos subyacentes.

  • ¿Qué tipo de clases y objetos admiten la semántica de movimiento? Depende de usted como desarrollador implementar la semántica de movimiento en sus propias clases si se beneficiarían de transferir a sus miembros en lugar de copiarlos. Una vez que implemente la semántica de movimiento, se beneficiará directamente del trabajo de muchos programadores de bibliotecas que han agregado soporte para manejar clases con semántica de movimiento de manera eficiente.

  • ¿Por qué el compilador no puede resolverlo por sí solo? El compilador no puede simplemente llamar a otra sobrecarga de una función a menos que usted lo diga. Debe ayudar al compilador a elegir si se debe llamar a la versión de función regular o mover.

  • ¿En qué situaciones me gustaría decirle al compilador que debe tratar una variable como un valor r? Lo más probable es que esto suceda en las funciones de plantilla o biblioteca, donde sabe que se puede recuperar un resultado intermedio.

Christopher Oezbek
fuente
2
Gran +1 para ejemplos de código con semántica en los comentarios. Las otras respuestas principales definen std :: move usando "move" en sí, ¡realmente no aclara nada! --- Creo que vale la pena mencionar que no hacer una copia del argumento significa que el valor original no se puede usar de manera confiable.
ty
34

std :: move en sí realmente no hace mucho. Pensé que llamaba al constructor movido para un objeto, pero en realidad solo realiza una conversión de tipo (convirtiendo una variable lvalue en un rvalue para que dicha variable pueda pasarse como argumento a un constructor de movimiento u operador de asignación).

Por lo tanto, std :: move solo se usa como precursor para usar la semántica de movimiento. La semántica de movimiento es esencialmente una forma eficiente de tratar con objetos temporales.

Considerar objeto A = B + C + D + E + F;

Este es un código atractivo, pero E + F produce un objeto temporal. Entonces D + temp produce otro objeto temporal y así sucesivamente. En cada operador "+" normal de una clase, se producen copias profundas.

Por ejemplo

Object Object::operator+ (const Object& rhs) {
    Object temp (*this);
    // logic for adding
    return temp;
}

La creación del objeto temporal en esta función es inútil: estos objetos temporales se eliminarán al final de la línea de todos modos a medida que salgan del alcance.

Podemos utilizar la semántica de movimiento para "saquear" los objetos temporales y hacer algo como

 Object& Object::operator+ (Object&& rhs) {
     // logic to modify rhs directly
     return rhs;
 }

Esto evita que se realicen copias innecesarias en profundidad. Con referencia al ejemplo, la única parte donde se produce una copia profunda ahora es E + F. El resto usa semántica de movimiento. El constructor de movimientos u operador de asignación también debe implementarse para asignar el resultado a A.

usuario929404
fuente
3
hablaste sobre semántica de movimientos. debe agregar a su respuesta cómo std :: move se puede usar porque la pregunta se refiere a eso.
Koushik Shetty
2
@Koushik std :: move no hace mucho, pero se usa para implementar la semántica de movimiento. Si no conoce std :: move, probablemente tampoco conozca la semántica de movimiento
user929404
1
"no hace mucho" (sí, solo una static_cast a una referencia rvalue). lo que realmente hace y hace es lo que preguntó el OP. no necesita saber cómo funciona std :: move, pero debe saber qué hace la semántica de movimiento. además, "pero se usa para implementar semántica de movimiento" es al revés. conozca la semántica de movimiento y comprenderá std :: move, de lo contrario no. move solo ayuda en el movimiento y en sí mismo usa la semántica de movimiento. std :: move no hace más que convertir su argumento a rvalue reference, que es lo que requiere la semántica de move.
Koushik Shetty
10
"pero E + F produce un objeto temporal": el operador +va de izquierda a derecha, no de derecha a izquierda. Por B+Clo tanto , sería el primero!
Ajay
8

"¿Qué es?" y "¿Qué hace?" ha sido explicado anteriormente.

Daré un ejemplo de "cuándo debería usarse".

Por ejemplo, tenemos una clase con muchos recursos como gran matriz.

class ResHeavy{ //  ResHeavy means heavy resource
    public:
        ResHeavy(int len=10):_upInt(new int[len]),_len(len){
            cout<<"default ctor"<<endl;
        }

        ResHeavy(const ResHeavy& rhs):_upInt(new int[rhs._len]),_len(rhs._len){
            cout<<"copy ctor"<<endl;
        }

        ResHeavy& operator=(const ResHeavy& rhs){
            _upInt.reset(new int[rhs._len]);
            _len = rhs._len;
            cout<<"operator= ctor"<<endl;
        }

        ResHeavy(ResHeavy&& rhs){
            _upInt = std::move(rhs._upInt);
            _len = rhs._len;
            rhs._len = 0;
            cout<<"move ctor"<<endl;
        }

    // check array valid
    bool is_up_valid(){
        return _upInt != nullptr;
    }

    private:
        std::unique_ptr<int[]> _upInt; // heavy array resource
        int _len; // length of int array
};

Código de prueba:

void test_std_move2(){
    ResHeavy rh; // only one int[]
    // operator rh

    // after some operator of rh, it becomes no-use
    // transform it to other object
    ResHeavy rh2 = std::move(rh); // rh becomes invalid

    // show rh, rh2 it valid
    if(rh.is_up_valid())
        cout<<"rh valid"<<endl;
    else
        cout<<"rh invalid"<<endl;

    if(rh2.is_up_valid())
        cout<<"rh2 valid"<<endl;
    else
        cout<<"rh2 invalid"<<endl;

    // new ResHeavy object, created by copy ctor
    ResHeavy rh3(rh2);  // two copy of int[]

    if(rh3.is_up_valid())
        cout<<"rh3 valid"<<endl;
    else
        cout<<"rh3 invalid"<<endl;
}

salida de la siguiente manera:

default ctor
move ctor
rh invalid
rh2 valid
copy ctor
rh3 valid

Podemos ver que std::movecon move constructormake transforma el recurso fácilmente.

¿Dónde más es std::moveútil?

std::moveTambién puede ser útil al ordenar una matriz de elementos. Muchos algoritmos de ordenación (como la ordenación por selección y la ordenación por burbujas) funcionan intercambiando pares de elementos. Anteriormente, hemos tenido que recurrir a la semántica de copia para hacer el intercambio. Ahora podemos usar la semántica de movimiento, que es más eficiente.

También puede ser útil si queremos mover los contenidos administrados por un puntero inteligente a otro.

Citado:

Jayhello
fuente
0

Aquí hay un ejemplo completo, usando std :: move para un vector personalizado (simple)

Rendimiento esperado:

 c: [10][11]
 copy ctor called
 copy of c: [10][11]
 move ctor called
 moved c: [10][11]

Compilar como:

  g++ -std=c++2a -O2 -Wall -pedantic foo.cpp

Código:

#include <iostream>
#include <algorithm>

template<class T> class MyVector {
private:
    T *data;
    size_t maxlen;
    size_t currlen;
public:
    MyVector<T> () : data (nullptr), maxlen(0), currlen(0) { }
    MyVector<T> (int maxlen) : data (new T [maxlen]), maxlen(maxlen), currlen(0) { }

    MyVector<T> (const MyVector& o) {
        std::cout << "copy ctor called" << std::endl;
        data = new T [o.maxlen];
        maxlen = o.maxlen;
        currlen = o.currlen;
        std::copy(o.data, o.data + o.maxlen, data);
    }

    MyVector<T> (const MyVector<T>&& o) {
        std::cout << "move ctor called" << std::endl;
        data = o.data;
        maxlen = o.maxlen;
        currlen = o.currlen;
    }

    void push_back (const T& i) {
        if (currlen >= maxlen) {
            maxlen *= 2;
            auto newdata = new T [maxlen];
            std::copy(data, data + currlen, newdata);
            if (data) {
                delete[] data;
            }
            data = newdata;
        }
        data[currlen++] = i;
    }

    friend std::ostream& operator<<(std::ostream &os, const MyVector<T>& o) {
        auto s = o.data;
        auto e = o.data + o.currlen;;
        while (s < e) {
            os << "[" << *s << "]";
            s++;
        }
        return os;
    }
};

int main() {
    auto c = new MyVector<int>(1);
    c->push_back(10);
    c->push_back(11);
    std::cout << "c: " << *c << std::endl;
    auto d = *c;
    std::cout << "copy of c: " << d << std::endl;
    auto e = std::move(*c);
    delete c;
    std::cout << "moved c: " << e << std::endl;
}
Neil McGill
fuente