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:
#include <vector>
#include <numeric>
#include <iostream>
#include <stdlib.h>
#include <algorithm>
#include <iterator>
class X {
std::vector<int> a;
public:
X() {
std::generate_n(std::back_inserter(a), 32767, ::rand);
}
X(X const &x) {
a = x.a;
std::cout << "Copy ctor invoked\n";
}
int sum() { return std::accumulate(a.begin(), a.end(), 0); }
};
X func() {
return X();
}
int main() {
X x = func();
std::cout << "sum = " << x.sum();
return 0;
};
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 que vector
(a partir de C ++ 03) necesita poder copiar elementos, y ifstream
s no se puede copiar, está atascado con algunos unique_ptr
/ shared_ptr
, o algo en ese orden para poder ponerlos en un vector. Tenga en cuenta que incluso si (por ejemplo) que reserve
el espacio en el vector
por lo que estamos seguros de que nuestros ifstream
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 nuestros ifstream
objetos de una vector
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.
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.