Acabo de terminar de escuchar la entrevista de podcast de radio de Ingeniería de Software con Scott Meyers sobre C ++ 0x . La mayoría de las nuevas funciones tenían sentido para mí, y ahora estoy realmente entusiasmado con C ++ 0x, con la excepción de una. Todavía no obtengo semántica de movimiento ... ¿Qué es exactamente?
c++
c++-faq
c++11
move-semantics
dicroce
fuente
fuente
Respuestas:
Me resulta más fácil entender la semántica de movimiento con código de ejemplo. Comencemos con una clase de cadena muy simple que solo contiene un puntero a un bloque de memoria asignado por el montón:
Como elegimos administrar la memoria nosotros mismos, debemos seguir la regla de tres . Voy a aplazar la escritura del operador de asignación y solo implementaré el destructor y el constructor de copia por ahora:
El constructor de copia define lo que significa copiar objetos de cadena. El parámetro se
const string& that
une a todas las expresiones de tipo cadena que le permite hacer copias en los siguientes ejemplos:Ahora viene la idea clave de la semántica del movimiento. Tenga en cuenta que solo en la primera línea donde copiamos
x
es realmente necesaria esta copia profunda, porque es posible que deseemos inspeccionarx
más tarde y nos sorprendería mucho six
hubiera cambiado de alguna manera. ¿Te diste cuenta de cómo acabo de decirx
tres veces (cuatro veces si incluyes esta oración) y quise decir exactamente el mismo objeto cada vez? Llamamos expresiones comox
"valores".Los argumentos en las líneas 2 y 3 no son valores, sino valores, porque los objetos de cadena subyacentes no tienen nombres, por lo que el cliente no tiene forma de inspeccionarlos nuevamente en un momento posterior. Los valores r denotan objetos temporales que se destruyen en el siguiente punto y coma (para ser más precisos: al final de la expresión completa que contiene léxicamente el valor r). Esto es importante porque durante la inicialización de
b
yc
, podríamos hacer lo que quisiéramos con la cadena de origen, y el cliente no podría notar la diferencia !C ++ 0x introduce un nuevo mecanismo llamado "referencia de valor" que, entre otras cosas, nos permite detectar argumentos de valor mediante la sobrecarga de funciones. Todo lo que tenemos que hacer es escribir un constructor con un parámetro de referencia rvalue. Dentro de ese constructor podemos hacer lo que queramos con la fuente, siempre que lo dejemos en un estado válido:
Que hemos hecho aqui En lugar de copiar profundamente los datos del montón, acabamos de copiar el puntero y luego establecer el puntero original en nulo (para evitar que 'eliminar []' del destructor del objeto fuente libere nuestros 'datos recién robados'). En efecto, hemos "robado" los datos que originalmente pertenecían a la cadena de origen. Una vez más, la idea clave es que bajo ninguna circunstancia el cliente podría detectar que la fuente había sido modificada. Como realmente no hacemos una copia aquí, llamamos a este constructor un "constructor de movimientos". Su trabajo es mover recursos de un objeto a otro en lugar de copiarlos.
¡Felicitaciones, ahora comprende los conceptos básicos de la semántica de movimientos! Continuemos implementando el operador de asignación. Si no está familiarizado con el idioma de copiar e intercambiar , apréndalo y regrese, porque es un idioma increíble de C ++ relacionado con la seguridad de excepciones.
Huh, eso es todo? "¿Dónde está la referencia de valor?" usted puede preguntar "¡No lo necesitamos aquí!" es mi respuesta :)
Tenga en cuenta que pasamos el parámetro
that
por valor , por lothat
que debe inicializarse como cualquier otro objeto de cadena. ¿Exactamente cómo sethat
va a inicializar? En los viejos tiempos de C ++ 98 , la respuesta habría sido "por el constructor de la copia". En C ++ 0x, el compilador elige entre el constructor de copia y el constructor de movimiento en función de si el argumento para el operador de asignación es un valor l o un valor r.Entonces, si usted dice
a = b
, el constructor de la copia se inicializaráthat
(porque la expresiónb
es un valor l), y el operador de asignación intercambia el contenido con una copia profunda recién creada. Esa es la definición misma del idioma de copiar e intercambiar: hacer una copia, intercambiar el contenido con la copia y luego deshacerse de la copia dejando el alcance. Nada nuevo aquí.Pero si dices
a = x + y
, el constructor de movimiento se inicializaráthat
(porque la expresiónx + y
es un valor r), por lo que no hay una copia profunda involucrada, solo un movimiento eficiente.that
sigue siendo un objeto independiente del argumento, pero su construcción fue trivial, ya que los datos del montón no tuvieron que ser copiados, solo movidos. No fue necesario copiarlo porquex + y
es un valor r, y nuevamente, está bien moverse de objetos de cadena denotados por valores.Para resumir, el constructor de copia realiza una copia profunda, porque la fuente debe permanecer intacta. El constructor de movimiento, por otro lado, solo puede copiar el puntero y luego establecer el puntero en la fuente en nulo. Está bien "anular" el objeto fuente de esta manera, porque el cliente no tiene forma de inspeccionar el objeto nuevamente.
Espero que este ejemplo haya entendido el punto principal. Hay mucho más para valorar las referencias y mover la semántica que dejé intencionalmente para simplificar. Si desea más detalles, consulte mi respuesta complementaria .
fuente
that.data = 0
, los personajes serían destruidos demasiado pronto (cuando el temporal muere), y también dos veces. ¡Desea robar los datos, no compartirlos!delete[]
en un nullptr está definido por el estándar C ++ como no operativo.Mi primera respuesta fue una introducción extremadamente simplificada para mover la semántica, y se omitieron muchos detalles a propósito para que fuera simple. Sin embargo, hay mucho más para mover la semántica, y pensé que era hora de una segunda respuesta para llenar los vacíos. La primera respuesta ya es bastante antigua, y no me pareció correcto simplemente reemplazarla con un texto completamente diferente. Creo que todavía sirve como una primera introducción. Pero si quieres profundizar más, sigue leyendo :)
Stephan T. Lavavej se tomó el tiempo para proporcionar comentarios valiosos. Muchas gracias, Stephan!
Introducción
La semántica de movimiento permite que un objeto, bajo ciertas condiciones, tome posesión de los recursos externos de otro objeto. Esto es importante de dos maneras:
Convirtiendo copias caras en movimientos baratos. Vea mi primera respuesta para un ejemplo. Tenga en cuenta que si un objeto no administra al menos un recurso externo (ya sea directa o indirectamente a través de sus objetos miembros), la semántica de movimiento no ofrecerá ninguna ventaja sobre la semántica de copia. En ese caso, copiar un objeto y mover un objeto significa exactamente lo mismo:
Implementación de tipos seguros de "solo movimiento"; es decir, los tipos para los que copiar no tiene sentido, pero el movimiento sí. Los ejemplos incluyen bloqueos, identificadores de archivos e indicadores inteligentes con una semántica de propiedad única. Nota: Esta respuesta trata sobre
std::auto_ptr
una plantilla de biblioteca estándar C ++ 98 en desuso, que fue reemplazada porstd::unique_ptr
en C ++ 11. Los programadores intermedios de C ++ probablemente estén al menos algo familiarizadosstd::auto_ptr
, y debido a la "semántica de movimiento" que muestra, parece un buen punto de partida para discutir la semántica de movimiento en C ++ 11. YMMV.¿Qué es un movimiento?
La biblioteca estándar C ++ 98 ofrece un puntero inteligente con una semántica de propiedad única llamada
std::auto_ptr<T>
. En caso de que no esté familiarizadoauto_ptr
, su propósito es garantizar que siempre se libere un objeto asignado dinámicamente, incluso ante excepciones:Lo inusual
auto_ptr
es su comportamiento de "copia":Nota cómo la inicialización de
b
cona
qué no copiar el triángulo, sino que transfiere la propiedad del triángulo dea
ab
. También decimos "a
se mueve ab
" o "el triángulo se mueve dea
ab
". Esto puede sonar confuso porque el triángulo siempre permanece en el mismo lugar en la memoria.El constructor de copia de
auto_ptr
probablemente se parece a esto (algo simplificado):Movimientos peligrosos e inofensivos.
Lo peligroso de esto
auto_ptr
es que lo que parece sintácticamente una copia es en realidad un movimiento. Intentar llamar a una función miembro en unauto_ptr
traspaso invocará un comportamiento indefinido, por lo que debe tener mucho cuidado de no usar unauto_ptr
después de que se haya movido desde:Pero
auto_ptr
no siempre es peligroso. Las funciones de fábrica son un caso de uso perfecto paraauto_ptr
:Observe cómo ambos ejemplos siguen el mismo patrón sintáctico:
Y, sin embargo, uno de ellos invoca un comportamiento indefinido, mientras que el otro no. Entonces, ¿cuál es la diferencia entre las expresiones
a
ymake_triangle()
? ¿No son los dos del mismo tipo? De hecho lo son, pero tienen diferentes categorías de valor .Categorías de valor
Obviamente, debe haber alguna diferencia profunda entre la expresión
a
que denota unaauto_ptr
variable y la expresiónmake_triangle()
que denota la llamada de una función que devuelve unauto_ptr
by, creando así un nuevoauto_ptr
objeto temporal cada vez que se llama.a
es un ejemplo de un lvalue , mientras quemake_triangle()
es un ejemplo de un rvalue .Pasar de valores como
a
es peligroso, ya que más tarde podríamos intentar llamar a una función miembroa
invocando un comportamiento indefinido. Por otro lado, pasar de valores comomake_triangle()
es perfectamente seguro, porque después de que el constructor de copia haya hecho su trabajo, no podemos usar el temporal nuevamente. No hay expresión que denote dicho temporal; Si simplemente escribimos demake_triangle()
nuevo, obtenemos un temporal diferente . De hecho, el traslado temporal ya no está en la siguiente línea:Tenga en cuenta que las letras
l
yr
tienen un origen histórico en el lado izquierdo y en el lado derecho de una tarea. Esto ya no es cierto en C ++, porque hay valores que no pueden aparecer en el lado izquierdo de una asignación (como matrices o tipos definidos por el usuario sin un operador de asignación), y hay valores que pueden (todos los valores de los tipos de clase con un operador de asignación).Referencias de valor
Ahora entendemos que moverse de los valores es potencialmente peligroso, pero moverse de los valores es inofensivo. Si C ++ tuviera soporte de lenguaje para distinguir los argumentos de lvalue de los argumentos de rvalue, podríamos prohibir completamente el movimiento de los valores, o al menos hacer que el movimiento de los valores sea explícito en el sitio de la llamada, de modo que ya no nos por accidente.
La respuesta de C ++ 11 a este problema son las referencias de valor . Una referencia de valor r es un nuevo tipo de referencia que solo se une a valores r, y la sintaxis es
X&&
. La buena referencia antiguaX&
ahora se conoce como referencia lvalue . (Tenga en cuenta que noX&&
es una referencia a una referencia; no existe tal cosa en C ++).Si nos adentramos
const
en la mezcla, ya tenemos cuatro tipos diferentes de referencias. ¿A qué tipos de expresiones de tipo seX
pueden unir?En la práctica, te puedes olvidar
const X&&
. Estar restringido para leer valores no es muy útil.Conversiones implícitas
Las referencias de Rvalue pasaron por varias versiones. Desde la versión 2.1, una referencia de valor
X&&
también se une a todas las categorías de valores de un tipo diferenteY
, siempre que haya una conversión implícita deY
aX
. En ese caso,X
se crea un tipo temporal , y la referencia rvalue está vinculada a ese temporal:En el ejemplo anterior,
"hello world"
es un valor de tipo lconst char[12]
. Dado que no hay una conversión implícita deconst char[12]
a travésconst char*
destd::string
, un temporal de tipostd::string
se crea, yr
está obligado a que temporal. Este es uno de los casos en los que la distinción entre valores (expresiones) y temporales (objetos) es un poco borrosa.Mover constructores
Un ejemplo útil de una función con un
X&&
parámetro es el constructor de movimientoX::X(X&& source)
. Su propósito es transferir la propiedad del recurso administrado desde la fuente al objeto actual.En C ++ 11,
std::auto_ptr<T>
ha sido reemplazado por elstd::unique_ptr<T>
que aprovecha las referencias rvalue. Desarrollaré y discutiré una versión simplificada deunique_ptr
. Primero, encapsulamos un puntero sin formato y sobrecargamos los operadores->
y*
, por lo tanto, nuestra clase se siente como un puntero:El constructor toma posesión del objeto y el destructor lo elimina:
Ahora viene la parte interesante, el constructor de movimiento:
Este constructor de movimientos hace exactamente lo que hizo el
auto_ptr
constructor de copias, pero solo se puede suministrar con valores:La segunda línea no se compila, porque
a
es un valor l, pero el parámetrounique_ptr&& source
solo puede vincularse a valores. Esto es exactamente lo que queríamos; movimientos peligrosos nunca deben ser implícitos. La tercera línea compila muy bien, porquemake_triangle()
es un valor r. El movimiento constructor transferirá la propiedad de la que temporalc
. De nuevo, esto es exactamente lo que queríamos.Mover operadores de asignación
La última pieza que falta es el operador de asignación de movimiento. Su trabajo es liberar el antiguo recurso y adquirir el nuevo recurso de su argumento:
Observe cómo esta implementación del operador de asignación de movimiento duplica la lógica tanto del destructor como del constructor de movimiento. ¿Conoces el idioma de copiar y cambiar? También se puede aplicar para mover la semántica como el modismo de mover y cambiar:
Ahora que
source
es una variable de tipounique_ptr
, será inicializada por el constructor de movimiento; es decir, el argumento se moverá al parámetro. Todavía se requiere que el argumento sea un valor r, porque el constructor de movimiento tiene un parámetro de referencia rvalue. Cuando el flujo de control alcanza la llave de cierreoperator=
,source
se sale del alcance y libera el recurso antiguo automáticamente.Mudarse de lvalues
A veces, queremos pasar de los valores. Es decir, a veces queremos que el compilador trate un valor l como si fuera un valor r, por lo que puede invocar al constructor de movimiento, aunque pueda ser potencialmente inseguro. Para este propósito, C ++ 11 ofrece una plantilla de función de biblioteca estándar llamada
std::move
dentro del encabezado<utility>
. Este nombre es un poco desafortunado, porquestd::move
simplemente arroja un valor l a un valor r; sí no se mueve nada por sí misma. Simplemente permite moverse. Tal vez debería haber sido nombradostd::cast_to_rvalue
ostd::enable_move
, pero ahora estamos atrapados con el nombre.Así es como se mueve explícitamente desde un valor l:
Tenga en cuenta que después de la tercera línea,
a
ya no posee un triángulo. Está bien, porque al escribir explícitamentestd::move(a)
, dejamos en claro nuestras intenciones: "Estimado constructor, haz lo que quierasa
para inicializarc
; ya no me importaa
. Siéntete libre de seguir tu caminoa
".Xvalores
Tenga en cuenta que aunque
std::move(a)
es un valor r, su evaluación no crea un objeto temporal. Este enigma obligó al comité a introducir una tercera categoría de valor. Algo que se puede unir a una referencia de valor de lado derecho, a pesar de que no es un valor de lado derecho en el sentido tradicional, se denomina xValue (valor que expira). Los valores tradicionales fueron renombrados como valores ( valores puros).Tanto los valores como los valores x son valores. Los valores X y los valores son ambos valores (valores generalizados). Las relaciones son más fáciles de comprender con un diagrama:
Tenga en cuenta que solo los valores x son realmente nuevos; el resto solo se debe a renombrar y agrupar.
Salir de funciones
Hasta ahora, hemos visto movimientos en variables locales y en parámetros de función. Pero moverse también es posible en la dirección opuesta. Si una función regresa por valor, algún objeto en el sitio de la llamada (probablemente una variable local o temporal, pero podría ser cualquier tipo de objeto) se inicializa con la expresión después de la
return
declaración como argumento para el constructor de movimiento:Quizás sorprendentemente, los objetos automáticos (variables locales que no se declaran como
static
) también se pueden mover implícitamente fuera de las funciones:¿Cómo es que el constructor de movimientos acepta el valor l
result
como argumento? El alcance deresult
está a punto de finalizar, y se destruirá durante el desbobinado de la pila. Nadie podría quejarse después de que esoresult
hubiera cambiado de alguna manera; cuando el flujo de control vuelve a la persona que llama, ¡result
ya no existe! Por esa razón, C ++ 11 tiene una regla especial que permite devolver objetos automáticos de funciones sin tener que escribirstd::move
. De hecho, nunca debe usarstd::move
para mover objetos automáticos fuera de las funciones, ya que esto inhibe la "optimización del valor de retorno con nombre" (NRVO).Tenga en cuenta que en ambas funciones de fábrica, el tipo de retorno es un valor, no una referencia de valor. Las referencias de Rvalue siguen siendo referencias y, como siempre, nunca debe devolver una referencia a un objeto automático; la persona que llama terminaría con una referencia pendiente si engañaste al compilador para que acepte tu código, así:
Pasando a miembros
Tarde o temprano, vas a escribir código como este:
Básicamente, el compilador se quejará de que
parameter
es un valor. Si observa su tipo, verá una referencia de valor r, pero una referencia de valor simplemente significa "una referencia vinculada a un valor r"; ¡ no significa que la referencia en sí misma sea un valor! De hecho,parameter
es solo una variable ordinaria con un nombre. Puede usarparameter
tantas veces como desee dentro del cuerpo del constructor, y siempre denota el mismo objeto. Irse de manera implícita sería peligroso, de ahí que el lenguaje lo prohíba.La solución es habilitar manualmente el movimiento:
Se podría argumentar que
parameter
ya no se usa después de la inicialización demember
. ¿Por qué no hay una regla especial para insertar silenciosamentestd::move
como con los valores de retorno? Probablemente porque sería una carga excesiva para los implementadores del compilador. Por ejemplo, ¿qué pasa si el cuerpo del constructor estaba en otra unidad de traducción? Por el contrario, la regla del valor de retorno simplemente tiene que verificar las tablas de símbolos para determinar si el identificador después de que lareturn
palabra clave denota un objeto automático.También puede pasar el
parameter
por valor. Para tipos de solo movimiento comounique_ptr
, parece que todavía no hay un idioma establecido. Personalmente, prefiero pasar por valor, ya que causa menos desorden en la interfaz.Funciones especiales para miembros
C ++ 98 declara implícitamente tres funciones miembro especiales a pedido, es decir, cuando se necesitan en algún lugar: el constructor de copia, el operador de asignación de copia y el destructor.
Las referencias de Rvalue pasaron por varias versiones. Desde la versión 3.0, C ++ 11 declara dos funciones miembro especiales adicionales a pedido: el constructor de movimientos y el operador de asignación de movimientos. Tenga en cuenta que ni VC10 ni VC11 se ajustan a la versión 3.0 todavía, por lo que deberá implementarlos usted mismo.
Estas dos nuevas funciones miembro especiales solo se declaran implícitamente si ninguna de las funciones miembro especiales se declara manualmente. Además, si declara su propio constructor de movimientos u operador de asignación de movimientos, ni el constructor de copias ni el operador de asignación de copias se declararán implícitamente.
¿Qué significan estas reglas en la práctica?
Tenga en cuenta que el operador de asignación de copia y el operador de asignación de movimiento pueden fusionarse en un único operador de asignación unificado, tomando su argumento por valor:
De esta forma, el número de funciones miembro especiales para implementar cae de cinco a cuatro. Aquí hay una compensación entre seguridad de excepción y eficiencia, pero no soy un experto en este tema.
Reenvío de referencias ( anteriormente conocidas como referencias universales )
Considere la siguiente plantilla de función:
Puede esperar
T&&
que solo se una a los valores, porque a primera vista, parece una referencia de valor. Sin embargo, resulta queT&&
también se une a los valores:Si el argumento es un valor de tipo
X
,T
se deduce que esX
, porT&&
lo tanto, significaX&&
. Esto es lo que cualquiera esperaría. Pero si el argumento es un valor de tipoX
, debido a una regla especial,T
se deduce que esX&
, porT&&
lo tanto , significaría algo asíX& &&
. Pero puesto que C ++ todavía no tiene noción de referencias a referencias, el tipoX& &&
está colapsado enX&
. Esto puede sonar confuso e inútil al principio, pero el colapso de referencia es esencial para un reenvío perfecto (que no se discutirá aquí).Si desea restringir una plantilla de función a valores, puede combinar SFINAE con rasgos de tipo:
Implementación de movimiento
Ahora que comprende el colapso de referencias, así es como
std::move
se implementa:Como puede ver,
move
acepta cualquier tipo de parámetro gracias a la referencia de reenvíoT&&
y devuelve una referencia de valor. Lastd::remove_reference<T>::type
llamada a la metafunción es necesaria porque de lo contrario, para valores de tipoX
, el tipo de retorno seríaX& &&
, que colapsaríaX&
. Comot
siempre es un valor de l (recuerde que una referencia de valor de r con nombre es un valor de l), pero queremos vincularnost
a una referencia de valor de r, tenemos que convertir explícitamentet
al tipo de retorno correcto. La llamada de una función que devuelve una referencia rvalue es en sí misma un xvalue. Ahora ya sabes de dónde vienen los valores x;)Tenga en cuenta que la devolución por referencia rvalue está bien en este ejemplo, porque
t
no denota un objeto automático, sino un objeto que fue pasado por la persona que llama.fuente
La semántica de movimiento se basa en referencias de valor .
Un valor r es un objeto temporal que se destruirá al final de la expresión. En C ++ actual, los valores solo se unen a
const
referencias. C ++ 1x permitiráconst
referencias no de valor, deletreadasT&&
, que son referencias a objetos de valor de valor.Como un valor r va a morir al final de una expresión, puede robar sus datos . En lugar de copiarlo en otro objeto, mueve sus datos a él.
En el código anterior, con viejos compiladores el resultado de
f()
se copian enx
el uso deX
's constructor de copia. Si su compilador admite la semántica de movimiento yX
tiene un constructor de movimiento, se llama en su lugar. Dado que surhs
argumento es un valor r , sabemos que ya no es necesario y podemos robarle su valor.Por lo tanto, el valor se mueve del temporal sin nombre devuelto de
f()
ax
(mientras que los datos dex
, inicializados en un vacíoX
, se mueven al temporal, que se destruirá después de la asignación).fuente
this->swap(std::move(rhs));
porque las referencias de rvalue nombradas son valoresrhs
es un valor-I en el contexto deX::X(X&& rhs)
.std::move(rhs)
Debe llamar para obtener un valor r, pero esto hace que la respuesta sea discutible.Supongamos que tiene una función que devuelve un objeto sustancial:
Cuando escribes código como este:
entonces un compilador de C ++ ordinario creará un objeto temporal para el resultado de
multiply()
, llamará al constructor de copia para que se inicialicer
, y luego destruirá el valor de retorno temporal. La semántica de movimiento en C ++ 0x permite que se llame al "constructor de movimiento" para que se inicier
copiando su contenido y luego descarte el valor temporal sin tener que destruirlo.Esto es especialmente importante si (como quizás el
Matrix
ejemplo anterior), el objeto que se copia asigna memoria adicional en el montón para almacenar su representación interna. Un constructor de copias tendría que hacer una copia completa de la representación interna, o usar el conteo de referencias y la semántica de copia en escritura de manera interna. Un constructor de movimientos dejaría sola la memoria de almacenamiento dinámico y simplemente copiaría el puntero dentro delMatrix
objeto.fuente
Si está realmente interesado en una buena explicación en profundidad de la semántica de movimiento, le recomiendo leer el documento original sobre ellos, "Una propuesta para agregar soporte de semántica de movimiento al lenguaje C ++".
Es muy accesible y fácil de leer y es un excelente caso para los beneficios que ofrecen. Hay otros documentos más recientes y actualizados sobre la semántica de movimientos disponibles en el sitio web WG21 , pero este es probablemente el más sencillo, ya que aborda las cosas desde una vista de nivel superior y no entra mucho en los detalles del lenguaje arenoso.
fuente
La semántica de movimiento se trata de transferir recursos en lugar de copiarlos cuando ya nadie necesita el valor fuente.
En C ++ 03, los objetos a menudo se copian, solo para ser destruidos o asignados antes de que cualquier código vuelva a usar el valor. Por ejemplo, cuando regresa por valor de una función, a menos que RVO entre en acción, el valor que está devolviendo se copia en el marco de la pila de la persona que llama, y luego se sale del alcance y se destruye. Este es solo uno de muchos ejemplos: vea el paso por valor cuando el objeto fuente es temporal, los algoritmos como
sort
ese simplemente reorganizan los elementos, la reasignaciónvector
cuandocapacity()
se excede, etc.Cuando tales pares de copia / destrucción son caros, generalmente se debe a que el objeto posee algún recurso pesado. Por ejemplo,
vector<string>
puede poseer un bloque de memoria asignado dinámicamente que contiene una matriz destring
objetos, cada uno con su propia memoria dinámica. Copiar un objeto de este tipo es costoso: debe asignar nueva memoria para cada bloque asignado dinámicamente en el origen y copiar todos los valores. Entonces necesita desasignar toda esa memoria que acaba de copiar. Sin embargo, mover un archivo grandevector<string>
significa simplemente copiar algunos punteros (que se refieren al bloque de memoria dinámica) al destino y ponerlos a cero en la fuente.fuente
En términos fáciles (prácticos):
Copiar un objeto significa copiar sus miembros "estáticos" y llamar al
new
operador por sus objetos dinámicos. ¿Derecha?Sin embargo, mover un objeto (repito, desde un punto de vista práctico) implica solo copiar los punteros de los objetos dinámicos, y no crear nuevos.
Pero, ¿no es eso peligroso? Por supuesto, podría destruir un objeto dinámico dos veces (falla de segmentación). Entonces, para evitar eso, debe "invalidar" los punteros de origen para evitar destruirlos dos veces:
Ok, pero si muevo un objeto, el objeto fuente se vuelve inútil, ¿no? Por supuesto, pero en ciertas situaciones eso es muy útil. La más evidente es cuando invoco una función con un objeto anónimo (temporal, objeto rvalue, ..., puede llamarlo con diferentes nombres):
En esa situación, se crea un objeto anónimo, luego se copia al parámetro de función y luego se elimina. Entonces, aquí es mejor mover el objeto, porque no necesita el objeto anónimo y puede ahorrar tiempo y memoria.
Esto lleva al concepto de una referencia de "valor". Existen en C ++ 11 solo para detectar si el objeto recibido es anónimo o no. Creo que ya sabe que un "lvalue" es una entidad asignable (la parte izquierda del
=
operador), por lo que necesita una referencia con nombre a un objeto para poder actuar como un lvalue. Un valor r es exactamente lo contrario, un objeto sin referencias nombradas. Por eso, objeto anónimo y rvalue son sinónimos. Entonces:En este caso, cuando un objeto de tipo
A
debe ser "copiado", el compilador crea una referencia de valor o una referencia de valor de acuerdo a si el objeto pasado es nombrado o no. Cuando no, se llama a su constructor de movimiento y sabe que el objeto es temporal y puede mover sus objetos dinámicos en lugar de copiarlos, ahorrando espacio y memoria.Es importante recordar que los objetos "estáticos" siempre se copian. No hay formas de "mover" un objeto estático (objeto en la pila y no en el montón). Por lo tanto, la distinción "mover" / "copiar" cuando un objeto no tiene miembros dinámicos (directa o indirectamente) es irrelevante.
Si su objeto es complejo y el destructor tiene otros efectos secundarios, como llamar a la función de una biblioteca, llamar a otras funciones globales o lo que sea, quizás sea mejor señalar un movimiento con una bandera:
Por lo tanto, su código es más corto (no necesita hacer una
nullptr
asignación para cada miembro dinámico) y más general.Otra pregunta típica: ¿cuál es la diferencia entre
A&&
yconst A&&
? Por supuesto, en el primer caso, puede modificar el objeto y en el segundo no, pero ¿significado práctico? En el segundo caso, no puede modificarlo, por lo que no tiene formas de invalidar el objeto (excepto con una bandera mutable o algo así), y no hay una diferencia práctica para un constructor de copias.¿Y qué es el reenvío perfecto ? Es importante saber que una "referencia de valor" es una referencia a un objeto nombrado en el "alcance del llamante". Pero en el ámbito real, una referencia rvalue es un nombre para un objeto, por lo tanto, actúa como un objeto con nombre. Si pasa una referencia de valor r a otra función, está pasando un objeto con nombre, por lo tanto, el objeto no se recibe como un objeto temporal.
El objeto
a
se copiará al parámetro real deother_function
. Si desea que el objetoa
continúe siendo tratado como un objeto temporal, debe usar lastd::move
función:Con esta línea, se
std::move
lanzaráa
a un valor r yother_function
recibirá el objeto como un objeto sin nombre. Por supuesto, siother_function
no tiene una sobrecarga específica para trabajar con objetos sin nombre, esta distinción no es importante.¿Es ese reenvío perfecto? No, pero estamos muy cerca. El reenvío perfecto solo es útil para trabajar con plantillas, con el propósito de decir: si necesito pasar un objeto a otra función, necesito que si recibo un objeto con nombre, el objeto se pase como un objeto con nombre, y cuando no, Quiero pasarlo como un objeto sin nombre:
Esa es la firma de una función prototípica que utiliza el reenvío perfecto, implementado en C ++ 11 por medio de
std::forward
. Esta función explota algunas reglas de creación de instancias de plantilla:Entonces, si
T
es una referencia de valor aA
( T = A &),a
también ( A & && => A &). SiT
es una referencia de valorA
,a
también (A && && => A &&). En ambos casos,a
es un objeto con nombre en el alcance real, peroT
contiene la información de su "tipo de referencia" desde el punto de vista del alcance del llamante. Esta información (T
) se pasa como parámetro de plantillaforward
y 'a' se mueve o no según el tipo deT
.fuente
Es como copiar la semántica, pero en lugar de tener que duplicar todos los datos, puede robar los datos del objeto desde el que se "mueve".
fuente
Sabes lo que significa una semántica de copia, ¿verdad? significa que tiene tipos que se pueden copiar, para los tipos definidos por el usuario, defina esto ya sea comprando explícitamente escribiendo un constructor de copia y un operador de asignación o el compilador los genera implícitamente. Esto hará una copia.
La semántica de movimiento es básicamente un tipo definido por el usuario con un constructor que toma una referencia de valor r (nuevo tipo de referencia usando && (sí, dos símbolos)) que no es constante, esto se llama un constructor de movimiento, lo mismo ocurre con el operador de asignación. Entonces, ¿qué hace un constructor de movimiento? Bueno, en lugar de copiar la memoria de su argumento de origen, 'mueve' la memoria del origen al destino.
¿Cuándo quieres hacer eso? Bien, std :: vector es un ejemplo, digamos que creó un std :: vector temporal y lo devuelve desde una función que dice:
Tendrá gastos generales del constructor de copia cuando la función regrese, si (y lo hará en C ++ 0x) std :: vector tiene un constructor de movimiento en lugar de copiarlo, solo puede configurar sus punteros y 'mover' dinámicamente asignado memoria a la nueva instancia. Es algo así como la semántica de transferencia de propiedad con std :: auto_ptr.
fuente
Para ilustrar la necesidad de la semántica de movimiento , consideremos este ejemplo sin semántica de movimiento:
Aquí hay una función que toma un objeto de tipo
T
y devuelve un objeto del mismo tipoT
:La función anterior utiliza la llamada por valor, lo que significa que cuando se llama a esta función, se debe construir un objeto para ser utilizado por la función.
Como la función también regresa por valor , se construye otro nuevo objeto para el valor de retorno:
Se han construido dos nuevos objetos, uno de los cuales es un objeto temporal que solo se usa durante la duración de la función.
Cuando se crea el nuevo objeto a partir del valor de retorno, se llama al constructor de copia para copiar el contenido del objeto temporal en el nuevo objeto b. Una vez que se completa la función, el objeto temporal utilizado en la función queda fuera de alcance y se destruye.
Ahora, consideremos lo que hace un constructor de copias .
Primero debe inicializar el objeto, luego copiar todos los datos relevantes del objeto antiguo al nuevo.
Dependiendo de la clase, quizás sea un contenedor con muchos datos, entonces eso podría representar mucho tiempo y uso de memoria
Con la semántica de movimiento ahora es posible hacer que la mayor parte de este trabajo sea menos desagradable simplemente moviendo los datos en lugar de copiarlos.
Mover los datos implica volver a asociar los datos con el nuevo objeto. Y ninguna copia tiene lugar en absoluto.
Esto se logra con una
rvalue
referencia.Una
rvalue
referencia funciona más o menos como unalvalue
referencia con una diferencia importante:una referencia rvalue se puede mover y un lvalue no.
Desde cppreference.com :
fuente
Estoy escribiendo esto para asegurarme de que lo entiendo correctamente.
La semántica de movimiento se creó para evitar la copia innecesaria de objetos grandes. Bjarne Stroustrup en su libro "El lenguaje de programación C ++" utiliza dos ejemplos en los que se producen copias innecesarias por defecto: uno, el intercambio de dos objetos grandes y dos, la devolución de un objeto grande de un método.
Intercambiar dos objetos grandes generalmente implica copiar el primer objeto a un objeto temporal, copiar el segundo objeto al primer objeto y copiar el objeto temporal al segundo objeto. Para un tipo integrado, esto es muy rápido, pero para objetos grandes estas tres copias pueden llevar una gran cantidad de tiempo. Una "asignación de movimiento" permite al programador anular el comportamiento de copia predeterminado y, en cambio, intercambiar referencias a los objetos, lo que significa que no hay ninguna copia y la operación de intercambio es mucho más rápida. La asignación de movimiento se puede invocar llamando al método std :: move ().
Devolver un objeto de un método por defecto implica hacer una copia del objeto local y sus datos asociados en una ubicación que sea accesible para la persona que llama (porque el objeto local no es accesible para la persona que llama y desaparece cuando finaliza el método). Cuando se devuelve un tipo incorporado, esta operación es muy rápida, pero si se devuelve un objeto grande, esto podría llevar mucho tiempo. El constructor de movimiento permite al programador anular este comportamiento predeterminado y, en su lugar, "reutilizar" los datos de almacenamiento dinámico asociados con el objeto local al señalar el objeto que se devuelve al llamante para acumular datos asociados con el objeto local. Por lo tanto, no se requiere copia.
En los lenguajes que no permiten la creación de objetos locales (es decir, objetos en la pila), estos tipos de problemas no ocurren ya que todos los objetos se asignan en el montón y siempre se accede a ellos por referencia.
fuente
x
yy
, no puede simplemente "intercambiar referencias a los objetos" ; puede ser que los objetos contengan punteros que hacen referencia a otros datos, y esos punteros pueden intercambiarse, pero los operadores de movimiento no están obligados a intercambiar nada. Pueden borrar los datos del objeto desde el cual se mueven, en lugar de preservar los datos de destino en el mismo.swap()
sin mover la semántica. "La asignación de movimiento se puede invocar llamando al método std :: move ()". - a veces es necesario usarlostd::move()
, aunque en realidad eso no mueve nada, solo le permite al compilador saber que el argumento es movible, a vecesstd::forward<>()
(con referencias de reenvío), y otras veces el compilador sabe que un valor se puede mover.Aquí hay una respuesta del libro "El lenguaje de programación C ++" de Bjarne Stroustrup. Si no desea ver el video, puede ver el texto a continuación:
Considera este fragmento. Volver de un operador + implica copiar el resultado fuera de la variable local
res
y en un lugar donde la persona que llama pueda acceder a él.Realmente no queríamos una copia; solo queríamos obtener el resultado de una función. Por lo tanto, debemos mover un Vector en lugar de copiarlo. Podemos definir el constructor de movimientos de la siguiente manera:
El && significa "referencia de valor r" y es una referencia a la que podemos vincular un valor r. "rvalue" 'está destinado a complementar "lvalue", que en términos generales significa "algo que puede aparecer en el lado izquierdo de una tarea". Por lo tanto, un valor r significa aproximadamente "un valor al que no se puede asignar", como un entero devuelto por una llamada a la función y la
res
variable local en el operador + () para Vectores.¡Ahora, la declaración
return res;
no se copiará!fuente