Vi código en algún lugar en el que alguien decidió copiar un objeto y luego moverlo a un miembro de datos de una clase. Esto me dejó confuso porque pensé que el objetivo de mudarse era evitar copiar. Aquí está el ejemplo:
struct S
{
S(std::string str) : data(std::move(str))
{}
};
Aquí están mis preguntas:
- ¿Por qué no estamos tomando una referencia a rvalue
str
? - ¿No será cara una copia, especialmente teniendo en cuenta algo así
std::string
? - ¿Cuál sería la razón por la que el autor decidiera hacer una copia y luego una mudanza?
- ¿Cuándo debería hacer esto yo mismo?
c++
c++11
move-semantics
user2030677
fuente
fuente
Respuestas:
Antes de responder a sus preguntas, parece que se está equivocando: tomar por valor en C ++ 11 no siempre significa copiar. Si se pasa un valor de lado derecho, que se trasladó (siempre que exista un movimiento constructor viables) en lugar de ser copiado. Y
std::string
tiene un constructor de movimientos.A diferencia de C ++ 03, en C ++ 11 a menudo es idiomático tomar parámetros por valor, por las razones que explicaré a continuación. Consulte también estas preguntas y respuestas en StackOverflow para obtener un conjunto más general de pautas sobre cómo aceptar parámetros.
Porque eso haría imposible pasar lvalores, como en:
Si
S
solo tuviera un constructor que acepta rvalues, lo anterior no se compilaría.Si pasa un valor de lado derecho, que se trasladó a
str
, y que finalmente se trasladó adata
. No se realizará ninguna copia. Si pasa un valor-I, por el contrario, que lvalue será copiado enstr
, y luego se trasladó adata
.Entonces, para resumir, dos movimientos para rvalues, una copia y un movimiento para lvalues.
En primer lugar, como mencioné anteriormente, el primero no siempre es una copia; y dicho esto, la respuesta es: " Porque es eficiente (los movimientos de
std::string
objetos son baratos) y simple ".Bajo el supuesto de que los movimientos son baratos (ignorando el SSO aquí), prácticamente se pueden ignorar cuando se considera la eficiencia general de este diseño. Si lo hacemos, tenemos una copia para lvalues (como tendríamos si aceptamos una referencia de lvalue a
const
) y ninguna copia para rvalues (mientras que aún tendríamos una copia si aceptamos una referencia de lvalue aconst
).Esto significa que tomar por valor es tan bueno como tomar por lvalor referencia a
const
cuándo se proporcionan lvalues, y mejor cuando se proporcionan rvalues.PD: Para proporcionar algo de contexto, creo que estas son las preguntas y respuestas a las que se refiere el OP.
fuente
const T&
paso de argumentos: en el peor de los casos (lvalue) esto es lo mismo, pero en el caso de un temporal solo tienes que mover el temporal. Ganar-ganar.data
miembro)? Tendría una copia incluso si la tomara por referencia de valor aconst
data
!Para entender por qué este es un buen patrón, debemos examinar las alternativas, tanto en C ++ 03 como en C ++ 11.
Tenemos el método C ++ 03 de tomar un
std::string const&
:en este caso, siempre se realizará una única copia. Si construye a partir de una cadena C sin procesar,
std::string
se construirá a, luego se copiará nuevamente: dos asignaciones.Existe el método C ++ 03 de tomar una referencia a a
std::string
y luego cambiarla a localstd::string
:esa es la versión C ++ 03 de "semántica de movimiento", y a
swap
menudo se puede optimizar para que sea muy barata de hacer (como amove
). También debe analizarse en contexto:y te obliga a formar una no temporal
std::string
, luego descartarla. (Un temporalstd::string
no se puede vincular a una referencia no constante). Sin embargo, solo se realiza una asignación. La versión de C ++ 11 tomaría un&&
y requeriría que lo llamaras constd::move
, o con un temporal: esto requiere que el llamador cree explícitamente una copia fuera de la llamada y mueva esa copia a la función o constructor.Utilizar:
A continuación, podemos hacer la versión completa de C ++ 11, que admite tanto la copia como
move
:Luego podemos examinar cómo se usa esto:
Está bastante claro que esta técnica de sobrecarga 2 es al menos tan eficiente, si no más, que los dos estilos de C ++ 03 anteriores. Llamaré a esta versión de 2 sobrecargas la versión "más óptima".
Ahora, examinaremos la versión de tomar por copia:
en cada uno de esos escenarios:
Si compara este lado a lado con la versión "más óptima", ¡hacemos exactamente uno adicional
move
! Ni una sola vez hacemos un extracopy
.Entonces, si asumimos que
move
es barata, esta versión nos ofrece casi el mismo rendimiento que la versión más óptima, pero 2 veces menos código.Y si está tomando, digamos, de 2 a 10 argumentos, la reducción en el código es exponencial: 2x veces menos con 1 argumento, 4x con 2, 8x con 3, 16x con 4, 1024x con 10 argumentos.
Ahora, podemos evitar esto a través del reenvío perfecto y SFINAE, lo que le permite escribir un solo constructor o plantilla de función que toma 10 argumentos, hace SFINAE para asegurarse de que los argumentos sean de los tipos apropiados y luego los mueve o copia en el estado local según sea necesario. Si bien esto evita el problema del aumento de mil veces en el tamaño del programa, todavía puede haber una gran cantidad de funciones generadas a partir de esta plantilla. (las instancias de funciones de plantilla generan funciones)
Y muchas funciones generadas significan un tamaño de código ejecutable más grande, lo que en sí mismo puede reducir el rendimiento.
Por el costo de unos pocos
move
segundos, obtenemos un código más corto y casi el mismo rendimiento y, a menudo, un código más fácil de entender.Ahora, esto solo funciona porque sabemos, cuando se llama a la función (en este caso, un constructor), que querremos una copia local de ese argumento. La idea es que si sabemos que vamos a hacer una copia, debemos informar a la persona que llama que estamos haciendo una copia poniéndola en nuestra lista de argumentos. Luego pueden optimizar en torno al hecho de que nos van a dar una copia (moviéndose a nuestro argumento, por ejemplo).
Otra ventaja de la técnica 'tomar por valor' es que a menudo los constructores de movimiento no son excepto. Eso significa que las funciones que toman por valor y se mueven fuera de su argumento a menudo pueden ser no excepto, moviendo cualquier
throw
s fuera de su cuerpo y dentro del alcance de llamada. (quién puede evitarlo a través de la construcción directa a veces, o construir los elementos ymove
en el argumento, para controlar dónde ocurre el lanzamiento) Hacer que los métodos no arrojen a menudo vale la pena.fuente
noexcept
. Al tomar datos por copia, puede realizar su funciónnoexcept
y hacer que cualquier construcción de copia provoque errores potenciales (como falta de memoria) fuera de la invocación de la función.Esto probablemente sea intencional y es similar al idioma de copiar e intercambiar . Básicamente, dado que la cadena se copia antes que el constructor, el constructor en sí es seguro para excepciones, ya que solo intercambia (mueve) la cadena temporal str.
fuente
No desea repetirse escribiendo un constructor para el movimiento y uno para la copia:
Esto es mucho código repetitivo, especialmente si tiene varios argumentos. Su solución evita la duplicación del costo de una mudanza innecesaria. (Sin embargo, la operación de movimiento debería ser bastante barata).
El idioma en competencia es usar el reenvío perfecto:
La plantilla mágica elegirá mover o copiar dependiendo del parámetro que le pase. Básicamente se expande a la primera versión, donde ambos constructores fueron escritos a mano. Para obtener información general, consulte la publicación de Scott Meyer sobre referencias universales .
Desde el punto de vista del rendimiento, la versión de reenvío perfecto es superior a su versión, ya que evita los movimientos innecesarios. Sin embargo, se puede argumentar que su versión es más fácil de leer y escribir. De todos modos, el posible impacto en el rendimiento no debería importar en la mayoría de las situaciones, por lo que al final parece ser una cuestión de estilo.
fuente