c++ - varias - copiar en circulo sketchup
¿Qué hace que mover objetos sea más rápido que copiar? (3)
Como @gudok respondió antes , todo está en la implementación ... Luego, un bit está en el código de usuario.
La implementación
Supongamos que estamos hablando del constructor de copia para asignar un valor a la clase actual.
La implementación que proporcionarás tendrá en cuenta dos casos:
- el parámetro es un valor l, por lo que no puede tocarlo, por definición
- el parámetro es un valor r, por lo que, de manera implícita, el temporal no vivirá mucho más tiempo que usted usándolo, por lo que, en lugar de copiar su contenido, podría robar su contenido
Ambos se implementan utilizando una sobrecarga:
Box::Box(const Box & other)
{
// copy the contents of other
}
Box::Box(Box && other)
{
// steal the contents of other
}
La implementación para clases ligeras.
Digamos que su clase contiene dos enteros: no puede robarlos porque son valores en bruto simples. Lo único que parecería robar sería copiar los valores, luego establecer el original en cero, o algo así ... Lo que no tiene sentido para los enteros simples. ¿Por qué ese trabajo extra?
Por lo tanto, para las clases de valor ligero, en realidad ofrece dos implementaciones específicas, una para valor l y otra para valor r, no tiene sentido.
Ofrecer solo la implementación de l-value será más que suficiente.
La implementación para clases más pesadas.
Pero en el caso de algunas clases pesadas (es decir, std :: string, std :: map, etc.), la copia implica potencialmente un costo, generalmente en las asignaciones. Así que, idealmente, quieres evitarlo tanto como sea posible. Aquí es donde robar los datos de los temporales se vuelve interesante.
Supongamos que su caja contiene un puntero en bruto a un HeavyResource
que es costoso copiar. El código se convierte en:
Box::Box(const Box & other)
{
this->p = new HeavyResource(*(other.p)) ; // costly copying
}
Box::Box(Box && other)
{
this->p = other.p ; // trivial stealing, part 1
other.p = nullptr ; // trivial stealing, part 2
}
Es claro que un constructor (el constructor de copia, que necesita una asignación) es mucho más lento que otro (el constructor de movimiento, que solo necesita asignaciones de punteros en bruto).
¿Cuándo es seguro "robar"?
El problema es que, por defecto, el compilador invocará el "código rápido" solo cuando el parámetro sea temporal (es un poco más sutil, pero tenga paciencia conmigo ...).
¿Por qué?
Porque el compilador puede garantizar que puedes robar de algún objeto sin ningún problema solo si ese objeto es temporal (o será destruido poco después). Para los otros objetos, robar significa que repentinamente tienes un objeto que es válido, pero en un estado no especificado, que aún podría usarse más abajo en el código. Posiblemente conduciendo a fallos o errores:
Box box3 = static_cast<Box &&>(box1); // calls the "stealing" constructor
box1.doSomething(); // Oops! You are using an "empty" object!
Pero a veces, quieres el rendimiento. ¿Entonces, cómo lo haces?
El codigo de usuario
Como escribiste:
Box box1 = some_value;
Box box2 = box1; // value of box1 is copied to box2 ... ok
Box box3 = std::move(box1); // ???
Lo que sucede para box2 es que, como box1 es un valor l, se invoca el primer constructor de copia "lento". Este es el código normal, C ++ 98.
Ahora, para box3, sucede algo gracioso: std :: move devuelve la misma caja1, pero como una referencia de valor r, en lugar de un valor l. Así que la línea:
Box box3 = ...
... NO invocará copia-constructor en box1.
Invocará a INSTEAD el constructor de robo (conocido oficialmente como move-constructor) en box1.
Y como su implementación del constructor de movimiento para Box "roba" el contenido de box1, al final de la expresión, box1 está en un estado válido pero no especificado (generalmente, estará vacío), y box3 contiene el (anterior) contenido de la caja1.
¿Qué pasa con el estado válido pero no especificado de una clase que se muda?
Por supuesto, escribir std :: move en un valor l significa que usted hace una promesa de que no usará ese valor l nuevamente. O lo harás, muy, muy cuidadosamente.
Citando el Borrador Estándar de C ++ 17 (C ++ 11 fue: 17.6.5.15):
20.5.5.15 Estado de origen de tipos de biblioteca [lib.types.movedfrom]
Los objetos de los tipos definidos en la biblioteca estándar de C ++ se pueden mover desde (15.8). Las operaciones de movimiento pueden especificarse explícitamente o generarse implícitamente. A menos que se especifique lo contrario, dichos objetos movidos desde se colocarán en un estado válido pero no especificado.
Esto fue sobre los tipos en la biblioteca estándar, pero esto es algo que debe seguir para su propio código.
Lo que significa es que el valor movido ahora podría contener cualquier valor, desde estar vacío, cero o algún valor aleatorio. Por ejemplo, por lo que sabe, su cadena "Hola" se convertiría en una cadena vacía "", o se convertiría en "Infierno", o incluso "Adiós", si el implementador considera que es la solución correcta. Sin embargo, aún debe ser una cadena válida, con todos sus invariantes respetados.
Entonces, al final, a menos que el implementador (de un tipo) se comprometa explícitamente con un comportamiento específico después de una mudanza, debe actuar como si no supiera nada acerca de un valor de mudanza (de ese tipo).
Conclusión
Como se dijo anteriormente, el std :: move no hace nada . Solo le dice al compilador: "¿Ves ese valor l? Por favor, consideralo un valor r, solo por un segundo".
Entonces, en
Box box3 = std::move(box1); // ???
... el código de usuario (es decir, std :: move) le dice al compilador que el parámetro se puede considerar como un valor r para esta expresión y, por lo tanto, se llamará al constructor de movimientos.
Para el autor del código (y el revisor del código), el código realmente indica que está bien robar el contenido de box1, para moverlo a box3. El autor del código tendrá que asegurarse de que box1 ya no se use (o se use con mucho cuidado). Es su responsabilidad.
Pero al final, es la implementación del constructor de movimientos lo que marcará una diferencia, principalmente en el rendimiento: si el constructor de movimientos realmente roba el contenido del valor r, verá una diferencia. Si hace algo más, entonces el autor mintió al respecto, pero este es otro problema ...
He escuchado a Scott Meyers decir " std::move()
no mueve nada" ... pero no he entendido lo que significa.
Entonces para especificar mi pregunta considera lo siguiente:
class Box { /* things... */ };
Box box1 = some_value;
Box box2 = box1; // value of box1 is copied to box2 ... ok
Qué pasa:
Box box3 = std::move(box1);
¿Entiendo las reglas de lvalue y rvalue pero lo que no entiendo es lo que realmente está sucediendo en la memoria? ¿Es simplemente copiar el valor de alguna manera diferente, compartir una dirección o qué? Más específicamente: ¿qué hace que el movimiento sea más rápido que copiar?
Siento que entender esto me dejaría todo claro. ¡Gracias por adelantado!
EDITAR: Tenga en cuenta que no estoy preguntando acerca de la implementación de std::move()
o cualquier otra cosa sintáctica.
La función std::move()
debe entenderse como una conversión al tipo rvalue correspondiente, que permite mover el objeto en lugar de copiarlo.
Podría no hacer ninguna diferencia en absoluto:
std::cout << std::move(std::string("Hello, world!")) << std::endl;
Aquí, la cadena ya era un valor, por lo que std::move()
no cambió nada.
Puede habilitar el movimiento, pero aún puede resultar en una copia:
auto a = 42;
auto b = std::move(a);
No hay una forma más eficiente de crear un entero que simplemente lo copie.
Donde se producirá un movimiento es cuando el argumento
- es un lvalue, o lvalue reference,
- tiene un constructor de movimiento o un operador de asignación de movimiento , y
- es (implícita o explícitamente) la fuente de una construcción o asignación.
Incluso en este caso, no es el move()
que mueve los datos, es la construcción o la asignación. std:move()
es simplemente el elenco que permite que eso suceda, incluso si tiene un valor lval para comenzar. Y el movimiento puede ocurrir sin std::move
si comienzas con un valor de r. Creo que ese es el significado detrás de la declaración de Meyers.
Se trata de la implementación. Considere la clase de cadena simple:
class my_string {
char* ptr;
size_t capacity;
size_t length;
};
La semántica de la copia requiere que hagamos una copia completa de la cadena, incluida la asignación de otra matriz en la memoria dinámica y la copia *ptr
contenidos, lo que es costoso.
La semántica de movimiento requiere que solo transfiramos el valor del puntero a un nuevo objeto sin duplicar el contenido de la cadena.
Si, por supuesto, la clase no usa la memoria dinámica o los recursos del sistema, entonces no hay diferencia entre mover y copiar en términos de rendimiento.