Recientemente he estado leyendo sobre constructores de movimientos en C ++ (ver, por ejemplo, aquí ) y estoy tratando de entender cómo funcionan y cuándo debo usarlos.
Según tengo entendido, un constructor de movimiento se usa para aliviar los problemas de rendimiento causados por la copia de objetos grandes. La página de Wikipedia dice: "Un problema de rendimiento crónico con C ++ 03 son las copias profundas costosas e innecesarias que pueden suceder implícitamente cuando los objetos se pasan por valor".
Normalmente abordo tales situaciones
- pasando los objetos por referencia, o
- mediante el uso de punteros inteligentes (por ejemplo, boost :: shared_ptr) para pasar el objeto (los punteros inteligentes se copian en lugar del objeto).
¿En qué situaciones las dos técnicas anteriores no son suficientes y es más conveniente usar un constructor de movimientos?
c++
programming-practices
Giorgio
fuente
fuente
shared_ptr
copia simple) y si la semántica de movimiento puede lograr lo mismo casi sin penalización de codificación, semántica y limpieza.Respuestas:
La semántica de movimiento introduce una dimensión completa en C ++: no solo está ahí para permitirle devolver valores a bajo precio.
Por ejemplo, sin move-semantics
std::unique_ptr
no funciona: mirestd::auto_ptr
, lo que se desaprobó con la introducción de move-semantics y se eliminó en C ++ 17. Mover un recurso es muy diferente de copiarlo. Permite la transferencia de la propiedad de un artículo único.Por ejemplo, no miremos
std::unique_ptr
, ya que está bastante bien discutido. Veamos, digamos, un objeto de búfer de vértices en OpenGL. Un búfer de vértices representa la memoria en la GPU: debe asignarse y desasignarse utilizando funciones especiales, posiblemente con restricciones estrictas sobre cuánto tiempo puede vivir. También es importante que solo un propietario lo use.Ahora, esto podría hacerse con un
std::shared_ptr
- pero este recurso no se debe compartir. Esto hace que sea confuso usar un puntero compartido. Podrías usarlostd::unique_ptr
, pero eso aún requiere semántica de movimiento.Obviamente, no he implementado un constructor de movimiento, pero entiendes la idea.
Lo relevante aquí es que algunos recursos no se pueden copiar . Puede pasar punteros en lugar de moverse, pero a menos que use unique_ptr, existe el problema de la propiedad. Vale la pena ser lo más claro posible en cuanto a cuál es la intención del código, por lo que un constructor de movimientos es probablemente el mejor enfoque.
fuente
La semántica de movimiento no es necesariamente una mejora tan grande cuando devuelve un valor, y cuando / si usa un
shared_ptr
(o algo similar) probablemente sea pesimista prematuramente. En realidad, casi todos los compiladores razonablemente modernos hacen lo que se llama Return Value Optimization (RVO) y Named Return Value Optimization (NRVO). Esto significa que cuando devuelve un valor, en lugar de copiar el valor en absoluto, simplemente pasan un puntero / referencia oculto a donde se asignará el valor después del retorno, y la función lo usa para crear el valor donde va a terminar. El estándar C ++ incluye disposiciones especiales para permitir esto, por lo que incluso (por ejemplo) su constructor de copia tiene efectos secundarios visibles, no es necesario usar el constructor de copia para devolver el valor. Por ejemplo:La idea básica aquí es bastante simple: crear una clase con suficiente contenido que preferiríamos evitar copiar, si es posible (la
std::vector
llenamos con 32767 entradas aleatorias). Tenemos un copiador explícito que nos mostrará cuándo / si se copia. También tenemos un poco más de código para hacer algo con los valores aleatorios en el objeto, por lo que el optimizador no eliminará (al menos fácilmente) todo sobre la clase solo porque no hace nada.Luego tenemos un código para devolver uno de estos objetos de una función, y luego usamos la suma para asegurarnos de que el objeto realmente se crea, no solo se ignora por completo. Cuando lo ejecutamos, al menos con los compiladores más recientes / modernos, encontramos que el constructor de copias que escribimos nunca se ejecuta en absoluto, y sí, estoy bastante seguro de que incluso una copia rápida con un
shared_ptr
aún es más lenta que no copiar en absoluto.Mudarse le permite hacer una buena cantidad de cosas que simplemente no podría hacer (directamente) sin ellas. Considere la parte de "fusión" de un tipo de fusión externa: tiene, por ejemplo, 8 archivos que va a fusionar. Idealmente, le gustaría poner los 8 de esos archivos en un
vector
- pero dado quevector
(a partir de C ++ 03) necesita poder copiar elementos, yifstream
s no se puede copiar, está atascado con algunosunique_ptr
/shared_ptr
, o algo en ese orden para poder ponerlos en un vector. Tenga en cuenta que incluso si (por ejemplo) quereserve
el espacio en elvector
por lo que estamos seguros de que nuestrosifstream
nunca realmente se copiará s, el compilador no sabrá que, por lo que el código no se compilará a pesar de que sabemos que el constructor de copia nunca será utilizado de todos modos.Aunque todavía no se puede copiar, en C ++ 11
ifstream
se puede mover. En este caso, los objetos probablemente serán no siempre pueden mover, pero el hecho de que podrían ser si es necesario mantiene el compilador feliz, para que podamos poner nuestrosifstream
objetos de unavector
forma directa, sin ningún tipo de hacks puntero inteligente.Sin embargo, un vector que se expande es un ejemplo bastante decente de un tiempo en que la semántica de movimiento realmente puede ser útil. En este caso, RVO / NRVO no ayudará, porque no estamos tratando con el valor de retorno de una función (o algo muy similar). Tenemos un vector que contiene algunos objetos, y queremos mover esos objetos a una nueva porción de memoria más grande.
En C ++ 03, eso se hizo creando copias de los objetos en la nueva memoria, y luego destruyendo los objetos antiguos en la memoria anterior. Sin embargo, hacer todas esas copias solo para tirar las viejas era una pérdida de tiempo. En C ++ 11, puede esperar que se muevan en su lugar. Esto generalmente nos permite, en esencia, hacer una copia superficial en lugar de una copia profunda (generalmente mucho más lenta). En otras palabras, con una cadena o un vector (solo para un par de ejemplos) simplemente copiamos los punteros en los objetos, en lugar de hacer copias de todos los datos a los que se refieren esos punteros.
fuente
Considerar:
Al agregar cadenas a v, se expandirá según sea necesario, y en cada reasignación las cadenas deberán copiarse. Con los constructores de movimientos, esto es básicamente un problema.
Por supuesto, también puedes hacer algo como:
Pero eso funcionará bien solo porque
std::unique_ptr
implementa el constructor de movimiento.El uso
std::shared_ptr
tiene sentido solo en situaciones (raras) cuando realmente ha compartido la propiedad.fuente
string
tener una instancia deFoo
donde tiene 30 miembros de datos? ¿Launique_ptr
versión no sería más eficiente?Los valores de retorno son donde más me gustaría pasar por valor en lugar de algún tipo de referencia. Sería capaz de devolver rápidamente un objeto 'en la pila' sin una penalización de rendimiento masiva. Por otro lado, no es particularmente difícil evitar esto (los punteros compartidos son tan fáciles de usar ...), por lo que no estoy seguro de que realmente valga la pena hacer un trabajo adicional en mis objetos solo para poder hacer esto.
fuente