En el operador de asignación de una clase, generalmente debe verificar si el objeto asignado es el objeto de invocación para no arruinar las cosas:
Class& Class::operator=(const Class& rhs) {
if (this != &rhs) {
// do the assignment
}
return *this;
}
¿Necesita lo mismo para el operador de asignación de movimiento? ¿Hay alguna situación en la this == &rhs
que sería cierto?
? Class::operator=(Class&& rhs) {
?
}
c++
c++11
move-semantics
move-assignment-operator
Seth Carnegie
fuente
fuente
A a; a = std::move(a);
.std::move
es normal. Luego, tenga en cuenta los alias, y cuando esté dentro de una pila de llamadas y tenga una referenciaT
y otra referencia aT
... ¿va a verificar la identidad aquí? ¿Desea encontrar la primera llamada (o llamadas) donde documentar que no puede pasar el mismo argumento dos veces probará estáticamente que esas dos referencias no tendrán alias? ¿O harás que la autoasignación solo funcione?std::sort
ostd::shuffle
, cada vez que intercambia los elementosi
th yj
th de una matriz sin verificari != j
primero. (std::swap
se implementa en términos de asignación de movimiento).Respuestas:
Wow, hay mucho que limpiar aquí ...
Primero, Copiar e intercambiar no siempre es la forma correcta de implementar la Asignación de copia. Casi seguro en el caso de
dumb_array
, esta es una solución subóptima.El uso de Copy and Swap es
dumb_array
un ejemplo clásico de poner la operación más costosa con las características más completas en la capa inferior. Es perfecto para clientes que desean la función más completa y están dispuestos a pagar la penalización de rendimiento. Obtienen exactamente lo que quieren.Pero es desastroso para los clientes que no necesitan la función más completa y, en cambio, buscan el rendimiento más alto. Para ellos
dumb_array
es solo otra pieza de software que tienen que reescribir porque es demasiado lento. Si sedumb_array
hubiera diseñado de manera diferente, podría haber satisfecho a ambos clientes sin comprometer a ninguno de ellos.La clave para satisfacer a ambos clientes es construir las operaciones más rápidas en el nivel más bajo, y luego agregar API además de eso para funciones más completas a un costo más alto. Es decir, necesita la garantía de excepción fuerte, bien, usted paga por ello. ¿No lo necesitas? Aquí hay una solución más rápida.
Seamos concretos: aquí está el operador rápido y básico de la garantía de excepción Copiar asignación para
dumb_array
:Explicación:
Una de las cosas más caras que puede hacer en el hardware moderno es hacer un viaje al montón. Cualquier cosa que pueda hacer para evitar un viaje al montón es tiempo y esfuerzo bien invertidos. Los clientes de
dumb_array
bien pueden querer a menudo asignar matrices del mismo tamaño. Y cuando lo hacen, todo lo que necesitas hacer es unmemcpy
(oculto debajostd::copy
). ¡No desea asignar una nueva matriz del mismo tamaño y luego desasignar la anterior del mismo tamaño!Ahora para sus clientes que realmente desean una seguridad de excepción fuerte:
O tal vez si desea aprovechar la asignación de movimiento en C ++ 11 que debería ser:
Si
dumb_array
los clientes valoran la velocidad, deben llamar aloperator=
. Si necesitan una seguridad de excepción fuerte, hay algoritmos genéricos a los que pueden llamar que funcionarán en una amplia variedad de objetos y solo deberán implementarse una vez.Ahora volvamos a la pregunta original (que tiene un tipo-o en este momento):
Esta es en realidad una pregunta controvertida. Algunos dirán que sí, absolutamente, algunos dirán que no.
Mi opinión personal es no, no necesita este cheque.
Razón fundamental:
Cuando un objeto se une a una referencia de valor r es una de dos cosas:
Si tiene una referencia a un objeto que es un temporal real, entonces, por definición, tiene una referencia única a ese objeto. No puede ser referenciado por ningún otro lugar en todo su programa. Es decir,
this == &temporary
no es posible .Ahora, si su cliente le ha mentido y le ha prometido que recibirá un contrato temporal cuando no lo está, entonces es responsabilidad del cliente asegurarse de que no tenga que preocuparse. Si quieres tener mucho cuidado, creo que esta sería una mejor implementación:
Es decir, si se pasa una referencia propia, esto es un error por parte del cliente que debe ser fijo.
Para completar, aquí hay un operador de asignación de movimiento para
dumb_array
:En el caso de uso típico de la asignación de movimiento,
*this
será un objeto movido desde y, pordelete [] mArray;
lo tanto, debería ser un no-op. Es crítico que las implementaciones hagan que borrar en un nullptr sea lo más rápido posible.Consideración:
Algunos argumentarán que
swap(x, x)
es una buena idea, o simplemente un mal necesario. Y esto, si el intercambio va al intercambio predeterminado, puede causar una asignación de auto-movimiento.No estoy de acuerdo que
swap(x, x)
es siempre una buena idea. Si lo encuentro en mi propio código, lo consideraré un error de rendimiento y lo solucionaré. Pero en caso de que desee permitirlo,swap(x, x)
tenga en cuenta que solo se realiza la autoasignación de asignación de red en un valor movido desde. Y en nuestrodumb_array
ejemplo, esto será perfectamente inofensivo si simplemente omitimos la afirmación o la restringimos al caso de mudado:Si autoasigna dos movidos desde (vacíos)
dumb_array
, no hace nada incorrecto además de insertar instrucciones inútiles en su programa. Esta misma observación se puede hacer para la gran mayoría de los objetos.<
Actualizar>
He pensado un poco más sobre este tema y he cambiado un poco mi posición. Ahora creo que la asignación debe ser tolerante con la autoasignación, pero que las condiciones de publicación en la asignación de copia y la asignación de movimiento son diferentes:
Para la asignación de copia:
uno debe tener una condición posterior que el valor de
y
no debe ser alterado. Cuando&x == &y
entonces esto se traduce en una condición posterior: de asignación de copia auto debería tener ningún impacto en el valor dex
.Para la asignación de movimiento:
uno debe tener una condición posterior que
y
tenga un estado válido pero no especificado. Cuando&x == &y
entonces esta condición posterior se traduce en:x
tiene un estado válido pero no especificado. Es decir, la asignación de auto-movimiento no tiene que ser un no-op. Pero no debería estrellarse. Esta condición posterior es coherente con permitirswap(x, x)
simplemente trabajar:Lo anterior funciona, siempre y cuando
x = std::move(x)
no se bloquee. Puede salirx
en cualquier estado válido pero no especificado.Veo tres formas de programar el operador de asignación de movimiento para
dumb_array
lograr esto:La implementación anterior tolera asignación de uno mismo, pero
*this
yother
llegar a ser una matriz de tamaño cero después de la asignación de auto-movimiento, sin importar el valor original de*this
es. Esto esta bien.La implementación anterior tolera la autoasignación de la misma manera que lo hace el operador de asignación de copias, al hacer que no funcione. Esto también está bien.
Lo anterior está bien solo si
dumb_array
no contiene recursos que deberían destruirse "inmediatamente". Por ejemplo, si el único recurso es la memoria, lo anterior está bien. Sidumb_array
posiblemente pudiera mantener bloqueos de mutex o el estado abierto de los archivos, el cliente podría esperar razonablemente que esos recursos en la hora de la asignación de movimiento se liberen inmediatamente y, por lo tanto, esta implementación podría ser problemática.El costo de la primera es de dos tiendas adicionales. El costo del segundo es una prueba y rama. Ambos trabajan. Ambos cumplen con todos los requisitos de la Tabla 22 Requisitos de MoveAssignable en el estándar C ++ 11. El tercero también funciona en el módulo no relacionado con los recursos de memoria.
Las tres implementaciones pueden tener diferentes costos dependiendo del hardware: ¿Qué tan caro es una sucursal? ¿Hay muchos registros o muy pocos?
La conclusión es que la asignación de movimiento automático, a diferencia de la asignación de copia automática, no tiene que preservar el valor actual.
<
/Actualizar>
Una edición final (con suerte) inspirada en el comentario de Luc Danton:
Si está escribiendo una clase de alto nivel que no administra directamente la memoria (pero puede tener bases o miembros que sí la tienen), entonces la mejor implementación de la asignación de movimiento es a menudo:
Esto moverá asignar cada base y cada miembro a su vez, y no incluirá un
this != &other
cheque. Esto le proporcionará el rendimiento más alto y la seguridad de excepción básica, suponiendo que no se necesite mantener invariantes entre sus bases y miembros. Para sus clientes que exigen una fuerte seguridad de excepción, apúntelosstrong_assign
.fuente
std::swap(x,x)
, ¿por qué debería confiar en que manejará las operaciones más complicadas correctamente?std::swap(x,x)
llega, simplemente funciona incluso cuandox = std::move(x)
produce un resultado no especificado. ¡Intentalo! No tienes que creerme.swap
funciona siempre que sex = move(x)
vayax
en cualquier estado de movimiento. Y los algoritmosstd::copy
/std::move
se definen para producir un comportamiento indefinido en las copias no operativas (¡ay, el joven de 20 años entiendememmove
bien el caso trivial perostd::move
no lo hace!). Así que supongo que todavía no he pensado en un "slam dunk" para la autoasignación. Pero obviamente la autoasignación es algo que sucede mucho en el código real, ya sea que el Estándar lo haya bendecido o no.Primero, se equivocó la firma del operador de asignación de movimiento. Como los movimientos roban recursos del objeto fuente, la fuente tiene que ser una
const
referencia sin valor r.Tenga en cuenta que aún regresa a través de una referencia (no
const
) l -value.Para cualquier tipo de asignación directa, el estándar no es verificar la autoasignación, sino asegurarse de que una autoasignación no provoque un choque y una quemadura. En general, nadie lo hace de forma explícita
x = x
oy = std::move(y)
llamadas, pero aliasing, especialmente a través de múltiples funciones, puede dar lugara = b
oc = std::move(d)
en ser auto-asignaciones. Una verificación explícita de la autoasignación, es decirthis == &rhs
, que omite la carne de la función cuando es verdadera, es una forma de garantizar la seguridad de la autoasignación. Pero es una de las peores formas, ya que optimiza un caso raro (con suerte) mientras que es una anti-optimización para el caso más común (debido a ramificaciones y posiblemente errores de caché).Ahora, cuando (al menos) uno de los operandos es un objeto directamente temporal, nunca puede tener un escenario de autoasignación. Algunas personas abogan por asumir ese caso y optimizan tanto el código que el código se vuelve suicidamente estúpido cuando la suposición es incorrecta. Digo que descargar la verificación del mismo objeto en los usuarios es irresponsable. No hacemos ese argumento para la asignación de copias; ¿Por qué invertir la posición para la asignación de movimiento?
Hagamos un ejemplo, alterado de otro encuestado:
Esta asignación de copias maneja la autoasignación con gracia sin una verificación explícita. Si los tamaños de origen y destino difieren, la desasignación y la reasignación preceden a la copia. De lo contrario, solo se realiza la copia. La autoasignación no obtiene una ruta optimizada, se volca en la misma ruta que cuando los tamaños de origen y destino comienzan igual. La copia es técnicamente innecesaria cuando los dos objetos son equivalentes (incluso cuando son el mismo objeto), pero ese es el precio cuando no se realiza una verificación de igualdad (en cuanto al valor o la dirección) ya que dicho cheque en sí mismo sería un desperdicio más del tiempo. Tenga en cuenta que la autoasignación de objetos aquí causará una serie de autoasignaciones a nivel de elemento; El tipo de elemento debe ser seguro para hacer esto.
Al igual que su ejemplo de origen, esta asignación de copia proporciona la garantía de seguridad de excepción básica. Si desea una garantía sólida, utilice el operador de asignación unificada de la consulta original de Copiar e Intercambiar , que maneja la asignación de copia y movimiento. Pero el objetivo de este ejemplo es reducir la seguridad en un rango para ganar velocidad. (Por cierto, estamos asumiendo que los valores de los elementos individuales son independientes; que no existe una restricción invariable que limite algunos valores en comparación con otros).
Veamos una asignación de movimiento para este mismo tipo:
Un tipo intercambiable que necesita personalización debe tener una función libre de dos argumentos llamada
swap
en el mismo espacio de nombres que el tipo. (La restricción de espacio de nombres permite que las llamadas no calificadas se intercambien para trabajar). Un tipo de contenedor también debe agregar unaswap
función de miembro público para que coincida con los contenedores estándar. Siswap
no se proporciona un miembro , entonces la función libreswap
probablemente deba marcarse como amigo del tipo intercambiable. Si personaliza los movimientos para usarswap
, debe proporcionar su propio código de intercambio; el código estándar llama al código de movimiento del tipo, lo que daría como resultado una recursión mutua infinita para los tipos personalizados de movimiento.Al igual que los destructores, las funciones de intercambio y las operaciones de movimiento nunca deberían tirarse, si es posible, y probablemente marcadas como tales (en C ++ 11). Los tipos de biblioteca estándar y las rutinas tienen optimizaciones para los tipos de movimiento no tirables.
Esta primera versión de movimiento-asignación cumple el contrato básico. Los marcadores de recursos de la fuente se transfieren al objeto de destino. Los recursos antiguos no se filtrarán ya que el objeto fuente ahora los administra. Y el objeto fuente se deja en un estado utilizable donde se le pueden aplicar más operaciones, incluidas la asignación y la destrucción.
Tenga en cuenta que esta asignación de movimiento es automáticamente segura para la autoasignación, ya que la
swap
llamada es. También es muy seguro. El problema es la retención innecesaria de recursos. Conceptualmente, los recursos antiguos para el destino ya no son necesarios, pero aquí siguen existiendo solo para que el objeto de origen pueda permanecer válido. Si la destrucción programada del objeto fuente está muy lejos, estamos desperdiciando espacio de recursos, o peor si el espacio total de recursos es limitado y otras peticiones de recursos sucederán antes de que el (nuevo) objeto fuente muera oficialmente.Este problema es lo que causó el controvertido consejo actual del gurú sobre el auto-objetivo durante la asignación de movimientos. La forma de escribir la asignación de movimiento sin recursos persistentes es algo como:
La fuente se restablece a las condiciones predeterminadas, mientras que los recursos de destino antiguos se destruyen. En el caso de autoasignación, su objeto actual termina suicidándose. La forma principal de evitarlo es rodear el código de acción con un
if(this != &other)
bloque o atornillarlo y dejar que los clientes coman unaassert(this != &other)
línea inicial (si se siente bien).Una alternativa es estudiar cómo hacer que la asignación de copias sea una excepción segura, sin asignación unificada, y aplicarla a la asignación de movimiento:
Cuando
other
ythis
son distintos,other
se vacía por el movimiento haciatemp
y permanece así. Luegothis
pierde sus viejos recursostemp
mientras obtiene los recursos que originalmente teníaother
. Entonces los viejos recursos dethis
ser asesinado cuando lotemp
hace.Cuando ocurre la autoasignación, el vaciado de
other
a también setemp
vacíathis
. Luego, el objeto de destino recupera sus recursos cuandotemp
ethis
intercambia. La muerte detemp
reclama un objeto vacío, que debería ser prácticamente un no-op. El objetothis
/other
mantiene sus recursos.La asignación de movimiento nunca debe lanzarse, siempre que la construcción de movimiento y el intercambio también lo sean. El costo de estar a salvo también durante la autoasignación es unas pocas instrucciones más sobre los tipos de bajo nivel, que deben ser abrumados por la llamada de desasignación.
fuente
delete
a su segundo bloque de código?std::copy
provoca un comportamiento indefinido si los rangos de origen y destino se superponen (incluido el caso cuando coinciden). Ver C ++ 14 [alg.copy] / 3.Estoy en el campo de aquellos que quieren operadores seguros de autoasignación, pero no quieren escribir cheques de autoasignación en las implementaciones de
operator=
. Y, de hecho, ni siquiera quiero implementarlooperator=
, quiero que el comportamiento predeterminado funcione 'directamente'. Los mejores miembros especiales son aquellos que vienen gratis.Dicho esto, los requisitos de MoveAssignable presentes en el Estándar se describen a continuación (de 17.6.3.1 Requisitos de argumentos de plantilla [utility.arg.requirements], n3290):
donde los marcadores de posición se describen como: "
t
[es un] valor l modificable de tipo T;" y "rv
es un valor de tipo T;". Tenga en cuenta que esos son requisitos establecidos en los tipos utilizados como argumentos para las plantillas de la biblioteca Estándar, pero al mirar en otra parte del Estándar, noto que cada requisito en la asignación de movimiento es similar a este.Esto significa que
a = std::move(a)
tiene que ser 'seguro'. Si lo que necesita es una prueba de identidad (pthis != &other
. Ej. ), Hágalo, ¡de lo contrario no podrá colocar sus objetosstd::vector
! (A menos que no use aquellos miembros / operaciones que requieren MoveAssignable; pero no importa). Tenga en cuenta que con el ejemplo anteriora = std::move(a)
, entoncesthis == &other
se mantendrá.fuente
a = std::move(a)
no trabajar haría que una clase no funcionestd::vector
? ¿Ejemplo?std::vector<T>::erase
se permiten llamadas a menos queT
sea MoveAssignable. (Como un IIRC aparte, algunos requisitos de MoveAssignable se relajaron a MoveInsertable en su lugar en C ++ 14.)T
debe ser MoveAssignable, pero ¿por quéerase()
dependería alguna vez de mover un elemento a sí mismo ?A medida que
operator=
se escribe su función actual , dado que ha realizado el argumento rvalue-referenceconst
, no hay forma de que pueda "robar" los punteros y cambiar los valores de la referencia rvalue entrante ... simplemente no puede cambiarlo, usted solo podía leerlo. Solo vería un problema si comenzara a invocardelete
punteros, etc. en suthis
objeto como lo haría en unoperator=
método de referencia lvaue normal , pero ese tipo de derrota el punto de la versión rvalue ... es decir, sería Parece redundante usar la versión rvalue para hacer básicamente las mismas operaciones que normalmente se dejan a un métodoconst
-lvalueoperator=
.Ahora, si definió su
operator=
para tomar unaconst
referencia que no sea de valor, entonces la única forma en que podría ver que se requiere una verificación fue si pasó elthis
objeto a una función que devolvió intencionalmente una referencia de valor en lugar de una temporal.Por ejemplo, supongamos que alguien intenta escribir una
operator+
función y utiliza una combinación de referencias de valor y referencias de valor para "evitar" que se creen temporarios adicionales durante alguna operación de suma apilada en el tipo de objeto:Ahora, por lo que entiendo acerca de las referencias de valor, se desaconseja hacer lo anterior (es decir, debe devolver una referencia temporal, no de valor), pero, si alguien aún hiciera eso, entonces querría verificar para hacer asegúrese de que la referencia rvalue entrante no hacía referencia al mismo objeto que el
this
puntero.fuente
const
, entonces solo puede leer, por lo que la única necesidad es hacer una comprobación sería si decidierasoperator=(const T&&)
realizar la misma reinicialización de lathis
que harías en unoperator=(const T&)
método típico en lugar de una operación de estilo de intercambio (es decir, robar punteros, etc. en lugar de hacer copias profundas).Mi respuesta sigue siendo que la asignación de movimiento no tiene que salvarse contra la autoasignación, pero tiene una explicación diferente. Considere std :: unique_ptr. Si tuviera que implementar uno, haría algo como esto:
Si miras a Scott Meyers explicando esto, él hace algo similar. (Si vaga por qué no hacer un intercambio, tiene una escritura adicional). Y esto no es seguro para la autoasignación.
A veces esto es desafortunado. Considere salir del vector todos los números pares:
Esto está bien para los enteros, pero no creo que pueda hacer que algo como esto funcione con la semántica de movimiento.
Para concluir: mover la asignación al objeto en sí no está bien y hay que estar atento.
Pequeña actualización
swap(x, x)
debería funcionar. Los algoritmos aman estas cosas! Siempre es agradable cuando un caso de esquina simplemente funciona. (Y aún no he visto un caso en el que no sea gratis. Sin embargo, eso no significa que no exista).unique_ptr& operator=(unique_ptr&& u) noexcept { reset(u.release()); ...}
es seguro para la asignación automática.fuente
Hay una situación en la que (esto == rhs) puedo pensar. Para esta declaración: Myclass obj; std :: move (obj) = std :: move (obj)
fuente