c++ - operadores - operaciones en c
Mueva el operador de asignación y `if(this!=& Rhs)` (6)
Como su función actual operator=
está escrita, dado que ha hecho el argumento de referencia rvalue const
, no hay forma de que pueda "robar" los punteros y cambiar los valores de la referencia de valor r entrante ... simplemente no puede cambiar esto, solo puedes leer de él. Solo vería un problema si comenzaras a llamar a delete
on puninters, etc. en tu objeto como lo harías en un método normal de lvaue-reference operator=
, pero ese tipo de errores vencerán el punto de la versión de rvalue ... es decir, parecería redundante utilizar la versión rvalue para realizar básicamente las mismas operaciones que normalmente se dejan a un método const
-lvalue operator=
.
Ahora bien, si definió su operator=
para tomar una referencia de valor-no de const
, entonces la única forma en que podía ver que se requiriera una verificación era si pasaba el objeto a una función que devolvía intencionalmente una referencia rvalue en lugar de una temporal.
Por ejemplo, supongamos que alguien intentó escribir una función operator+
y utiliza una combinación de referencias rvalue y referencias lvalue para "evitar" que se creen temporarios adicionales durante alguna operación de adición apilada en el tipo de objeto:
struct A; //defines operator=(A&& rhs) where it will "steal" the pointers
//of rhs and set the original pointers of rhs to NULL
A&& operator+(A& rhs, A&& lhs)
{
//...code
return std::move(rhs);
}
A&& operator+(A&& rhs, A&&lhs)
{
//...code
return std::move(rhs);
}
int main()
{
A a;
a = (a + A()) + A(); //calls operator=(A&&) with reference bound to a
//...rest of code
}
Ahora, por lo que entiendo acerca de las referencias de valores, se desaconseja hacer lo anterior (es decir, se debe devolver una referencia temporal, no de valor razonable), pero, si alguien todavía hiciera eso, entonces usted querría verificar para hacer Asegúrese de que la referencia rvalue entrante no hace referencia al mismo objeto que this
puntero.
En el operador de asignación de una clase, generalmente debe verificar si el objeto que se le asignó es el objeto que lo invoca para no arruinarlo:
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? ¿Alguna vez hay una situación donde this == &rhs
sea cierto?
? Class::operator=(Class&& rhs) {
?
}
Estoy en el campo de aquellos que quieren operadores seguros de asignación automática, pero no quieren escribir verificaciones de autoasignación en las implementaciones de operator=
. Y, de hecho, ni siquiera quiero implementar operator=
en absoluto, quiero que el comportamiento predeterminado funcione ''directamente de fábrica''. 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 (a partir de 17.6.3.1 requisitos de argumentos de plantilla [requisitos de utilidad.arg.], n3290):
Expression Return type Return value Post-condition t = rv T& t t is equivalent to the value of rv before the assignment
donde los marcadores de posición se describen como: " t
[es un valor t
modificable de tipo T;" y " rv
es un valor r de tipo T". Tenga en cuenta que esos son los requisitos puestos en los tipos utilizados como argumentos para las plantillas de la biblioteca estándar, pero mirando en otro lugar en el 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 (por ejemplo, this != &other
), ¡adelante, o de lo contrario no podrá poner sus objetos en std::vector
! (A menos que no use esos miembros / operaciones que requieren MoveAssignable, pero no lo olvide). Tenga en cuenta que con el ejemplo anterior a = std::move(a)
, entonces this == &other
se mantendrá.
Hay una situación que (esto == rhs) puedo pensar. Para esta afirmación: Myclass obj; std :: move (obj) = std :: move (obj)
Mi respuesta es que la asignación de movimiento no tiene que guardarse en contra de la autoasignación, pero tiene una explicación diferente. Considere std :: unique_ptr. Si tuviera que implementar uno, haría algo como esto:
unique_ptr& operator=(unique_ptr&& x) {
delete ptr_;
ptr_ = x.ptr_;
x.ptr_ = nullptr;
return *this;
}
Si miras a Scott Meyers explicando esto, él hace algo similar. (Si vaga por qué no hacer intercambio, tiene una escritura adicional). Y esto no es seguro para la auto asignación.
A veces esto es desafortunado. Considera mover del vector todos los números pares:
src.erase(
std::partition_copy(src.begin(), src.end(),
src.begin(),
std::back_inserter(even),
[](int num) { return num % 2; }
).first,
src.end());
Esto está bien para enteros, pero no creo que puedas hacer que algo así funcione con la semántica de movimientos.
Para concluir: la asignación de movimiento al objeto en sí no está bien y debes tener cuidado con ella.
Pequeña actualización.
- No estoy de acuerdo con Howard, lo cual es una mala idea, pero aún así creo que la asignación de movimiento de objetos "movidos" debería funcionar, porque el
swap(x, x)
debería funcionar. ¡Algoritmos adoran estas cosas! Siempre es bueno cuando una caja de esquina simplemente funciona. (Y todavía tengo que ver un caso en el que no es gratis. Sin embargo, no significa que no exista). - Así es como se implementa la asignación de unique_ptrs en libc ++:
unique_ptr& operator=(unique_ptr&& u) noexcept { reset(u.release()); ...}
unique_ptr& operator=(unique_ptr&& u) noexcept { reset(u.release()); ...}
Es seguro para la asignación de movimiento propio. - Las Pautas principales piensan que debería estar bien moverse por sí mismo.
Primero, tienes la firma del operador de asignación de movimiento equivocada. Como los movimientos roban recursos del objeto fuente, la fuente debe ser una referencia de valor no const
.
Class &Class::operator=( Class &&rhs ) {
//...
return *this;
}
Tenga en cuenta que aún regresa a través de una referencia de valor l (sin const
).
Para cualquiera de los tipos de asignación directa, el estándar no es verificar la autoasignación, sino asegurarse de que la autoasignación no provoque un colapso y quema. En general, nadie hace explícitamente llamadas x = x
y = std::move(y)
, pero el aliasing, especialmente a través de funciones múltiples, puede llevar a = b
o c = std::move(d)
a ser autoasignaciones. Una verificación explícita de la autoasignación, es decir, this == &rhs
, que omite la función de la función cuando es verdadera es una manera de garantizar la seguridad de la asignación automática. Pero es una de las peores formas, ya que optimiza un (raro) caso raro mientras que es una anti-optimización para el caso más común (debido a la bifurcación y posiblemente fallas de caché).
Ahora cuando (al menos) uno de los operandos es un objeto directamente temporal, nunca puede tener un escenario de asignación automática. Algunas personas abogan por asumir ese caso y optimizar el código tanto que el código se convierte en una estupidez suicida cuando la suposición es incorrecta. Digo que es irresponsable dejar el cheque de objeto mismo a los usuarios. No hacemos ese argumento para la copia de asignación; ¿Por qué revertir la posición de movimiento de asignación?
Hagamos un ejemplo, alterado de otro encuestado:
dumb_array& dumb_array::operator=(const dumb_array& other)
{
if (mSize != other.mSize)
{
delete [] mArray;
mArray = nullptr; // clear this...
mSize = 0u; // ...and this in case the next line throws
mArray = other.mSize ? new int[other.mSize] : nullptr;
mSize = other.mSize;
}
std::copy(other.mArray, other.mArray + mSize, mArray);
return *this;
}
Esta asignación de copia maneja la auto-asignació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, sino que se vierte 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 comprobación de igualdad (en cuanto a los valores o la dirección) ya que dicho control sería un desperdicio del tiempo. Tenga en cuenta que la asignación de objetos aquí provocará una serie de autoasignaciones a nivel de elemento; el tipo de elemento tiene que ser seguro para hacer esto.
Al igual que su ejemplo fuente, esta copia de asignación 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 Copiar e Intercambiar , que maneja la asignación de copiar y mover. Pero el objetivo de este ejemplo es reducir la seguridad en un rango para ganar velocidad. (Por cierto, suponemos 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:
class dumb_array
{
//...
void swap(dumb_array& other) noexcept
{
// Just in case we add UDT members later
using std::swap;
// both members are built-in types -> never throw
swap( this->mArray, other.mArray );
swap( this->mSize, other.mSize );
}
dumb_array& operator=(dumb_array&& other) noexcept
{
this->swap( other );
return *this;
}
//...
};
void swap( dumb_array &l, dumb_array &r ) noexcept { l.swap( r ); }
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 del espacio de nombres permite que las llamadas no calificadas cambien para funcionar). Un tipo de contenedor también debe agregar una función de miembro de swap
público para que coincida con los contenedores estándar. Si no se proporciona un swap
miembro, entonces el swap
libre de funciones probablemente deba marcarse como amigo del tipo intercambiable. Si personaliza movimientos para usar swap
, entonces 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 por movimiento.
Al igual que los destructores, las funciones de intercambio y movimiento deben ser Never-throw si es posible, y probablemente estén marcadas como tales (en C ++ 11). Los tipos y rutinas de biblioteca estándar tienen optimizaciones para tipos de movimiento no arrojables.
Esta primera versión de mover-asignación cumple con 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 pueden aplicar otras 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 llamada de swap
es. También es fuertemente una excepción segura. El problema es la retención innecesaria de recursos. Los antiguos recursos para el destino ya no son necesarios, pero aquí solo están disponibles para que el objeto fuente pueda seguir siendo 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 ocurrirán antes de que el objeto fuente (nuevo) muera oficialmente.
Este problema es lo que causó el controvertido consejo actual del gurú con respecto a la autoselección durante la asignación de movimiento. La forma de escribir la asignación de movimiento sin recursos persistentes es algo así como:
class dumb_array
{
//...
dumb_array& operator=(dumb_array&& other) noexcept
{
delete [] this->mArray; // kill old resources
this->mArray = other.mArray;
this->mSize = other.mSize;
other.mArray = nullptr; // reset source
other.mSize = 0u;
return *this;
}
//...
};
La fuente se restablece a las condiciones predeterminadas, mientras que los recursos de destino anteriores se destruyen. En el caso de autoasignación, tu objeto actual termina cometiendo suicidio. La forma principal es rodear el código de acción con un bloque if(this != &other)
, o atornillarlo y dejar que los clientes coman una línea inicial assert(this != &other)
(si te sientes bien).
Una alternativa es estudiar cómo hacer que la asignación de copias sea excepcionalmente segura, sin asignación unificada, y aplicarla a la asignación de movimiento:
class dumb_array
{
//...
dumb_array& operator=(dumb_array&& other) noexcept
{
dumb_array temp{ std::move(other) };
this->swap( temp );
return *this;
}
//...
};
Cuando el other
y this
son distintos, el other
se vacía por el movimiento a la temp
y se mantiene así. Entonces this
pierde sus viejos recursos a la temp
mientras obtiene los recursos originalmente en poder de other
. Entonces los viejos recursos de this
se matan cuando lo hace la temp
.
Cuando ocurre la autoasignación, el vaciado de other
a la temp
vacía this
. Luego, el objeto objetivo recupera sus recursos cuando la temp
y this
intercambio. La muerte de la temp
reclama un objeto vacío, que debería ser prácticamente un no-op. El this
/ other
objeto mantiene sus recursos.
La asignación de movimiento debe ser nunca tirada, siempre que la construcción de movimiento y el intercambio también lo sean. El costo de estar seguro también durante la autoasignación es un poco más de instrucciones sobre los tipos de bajo nivel, que deberían ser superados por la llamada de desasignación.
Wow, hay mucho para limpiar aquí ...
En primer lugar, Copy and Swap no siempre es la forma correcta de implementar Copy Assignment. Casi con seguridad en el caso de dumb_array
, esta es una solución subóptima.
El uso de Copy and Swap es para dumb_array
es un ejemplo clásico de poner la operación más cara 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 máximo rendimiento. Para ellos, dumb_array
es simplemente otra pieza de software que deben reescribir porque es demasiado lenta. Si dumb_array
hubiera diseñado de forma diferente, podría haber satisfecho a ambos clientes sin comprometer a ninguno de los clientes.
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 con más gasto. Es decir, necesita la fuerte garantía de excepción, está bien, lo paga. No lo necesitas? Aquí hay una solución más rápida.
Hagamos concreto: aquí está el rápido y básico operador de garantía de copia de garantía para dumb_array
:
dumb_array& operator=(const dumb_array& other)
{
if (this != &other)
{
if (mSize != other.mSize)
{
delete [] mArray;
mArray = nullptr;
mArray = other.mSize ? new int[other.mSize] : nullptr;
mSize = other.mSize;
}
std::copy(other.mArray, other.mArray + mSize, mArray);
}
return *this;
}
Explicación:
Una de las cosas más caras que puede hacer en hardware moderno es hacer un viaje al montón. Todo lo que puede hacer para evitar un viaje al montón es tiempo y esfuerzo bien invertidos. Los clientes de dumb_array
pueden querer asignar matrices del mismo tamaño. Y cuando lo hacen, todo lo que necesitas hacer es un memcpy
(oculto en std::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 fuerte excepción de seguridad:
template <class C>
C&
strong_assign(C& lhs, C rhs)
{
swap(lhs, rhs);
return lhs;
}
O tal vez si desea aprovechar la asignación de movimiento en C ++ 11 que debería ser:
template <class C>
C&
strong_assign(C& lhs, C rhs)
{
lhs = std::move(rhs);
return lhs;
}
Si los clientes de dumb_array
valoran la velocidad, deben llamar al operator=
. Si necesitan una fuerte excepción de seguridad, existen algoritmos genéricos a los que pueden llamar que funcionarán en una amplia variedad de objetos y solo necesitan implementarse una vez.
Ahora volvamos a la pregunta original (que tiene un tipo-o en este momento):
Class&
Class::operator=(Class&& rhs)
{
if (this == &rhs) // is this check needed?
{
// ...
}
return *this;
}
Esta es en realidad una pregunta controvertida. Algunos dirán que sí, absolutamente, algunos dirán que no.
Mi opinión personal es que no, no necesitas este cheque.
Razón fundamental:
Cuando un objeto se une a una referencia rvalue, es una de dos cosas:
- Un temporal.
- Un objeto que la persona que llama quiere que usted crea es temporal.
Si tiene una referencia a un objeto que es un elemento temporal real, entonces, por definición, tiene una referencia única para ese objeto. No puede ser referenciado por ningún otro lugar en su programa completo. Es decir, this == &temporary
no es posible .
Ahora, si su cliente le mintió y le prometió que está recibiendo un subsidio temporal cuando no lo está, entonces es responsabilidad del cliente asegurarse de que no tiene que preocuparse. Si quieres ser realmente cuidadoso, creo que esta sería una mejor implementación:
Class&
Class::operator=(Class&& other)
{
assert(this != &other);
// ...
return *this;
}
Es decir, si le pasan una auto referencia, este es un error por parte del cliente que debe corregirse.
Para completar, aquí hay un operador de asignación de movimiento para dumb_array
:
dumb_array& operator=(dumb_array&& other)
{
assert(this != &other);
delete [] mArray;
mSize = other.mSize;
mArray = other.mArray;
other.mSize = 0;
other.mArray = nullptr;
return *this;
}
En el caso de uso típico de la asignación de movimiento, *this
será un objeto movido desde y así delete [] mArray;
debería ser un no-op. Es fundamental que las implementaciones hagan eliminar en un nullptr lo más rápido posible.
Advertencia:
Algunos argumentarán que el 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 movimiento automático.
No estoy de acuerdo con que el swap(x, x)
sea una buena idea. Si lo encuentro en mi propio código, lo consideraré un error de rendimiento y lo corregiré. Pero en caso de que quiera permitirlo, tenga en cuenta que swap(x, x)
solo hace self-move-assignemnet en un valor movido desde. Y en nuestro ejemplo de dumb_array
esto será perfectamente inofensivo si simplemente omitimos la afirmación, o la limitamos al caso movido desde:
dumb_array& operator=(dumb_array&& other)
{
assert(this != &other || mSize == 0);
delete [] mArray;
mSize = other.mSize;
mArray = other.mArray;
other.mSize = 0;
other.mArray = nullptr;
return *this;
}
Si usted asigna dos asignaciones dumb_array
(vacías), no hará nada incorrecto aparte de insertar instrucciones inútiles en su programa. Esta misma observación se puede hacer para la gran mayoría de los objetos.
<
Actualización >
He reflexionado un poco más sobre este tema y he cambiado un poco mi posición. Ahora creo que la asignación debe ser tolerante a 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:
x = y;
uno debe tener una condición posterior de que el valor de y
no deba alterarse. Cuando &x == &y
continuación, esta condición posterior se traduce en: la asignación de autocopia no debería tener ningún impacto en el valor de x
.
Para la asignación de movimiento:
x = std::move(y);
uno debe tener una condición posterior que y
tenga un estado válido pero no especificado. Cuando &x == &y
continuación, esta condición se traduce en: x
tiene un estado válido pero no especificado. Es decir, la asignación de movimiento propio no tiene por qué ser una operación no operativa. Pero no debería estrellarse. Esta condición posterior es consistente con permitir que swap(x, x)
funcione:
template <class T>
void
swap(T& x, T& y)
{
// assume &x == &y
T tmp(std::move(x));
// x and y now have a valid but unspecified state
x = std::move(y);
// x and y still have a valid but unspecified state
y = std::move(tmp);
// x and y have the value of tmp, which is the value they had on entry
}
Lo anterior funciona, siempre que x = std::move(x)
no se cuelgue. Puede dejar x
en cualquier estado válido pero no especificado.
Veo tres formas de programar el operador de asignación de movimiento para dumb_array
para lograr esto:
dumb_array& operator=(dumb_array&& other)
{
delete [] mArray;
// set *this to a valid state before continuing
mSize = 0;
mArray = nullptr;
// *this is now in a valid state, continue with move assignment
mSize = other.mSize;
mArray = other.mArray;
other.mSize = 0;
other.mArray = nullptr;
return *this;
}
La implementación anterior tolera la autoasignación, pero *this
y el other
terminan siendo una matriz de tamaño cero después de la asignación de movimiento propio, sin importar el valor original de *this
es. Esto esta bien.
dumb_array& operator=(dumb_array&& other)
{
if (this != &other)
{
delete [] mArray;
mSize = other.mSize;
mArray = other.mArray;
other.mSize = 0;
other.mArray = nullptr;
}
return *this;
}
La implementación anterior tolera la autoasignación de la misma manera que lo hace el operador de asignación de copias, al hacerlo no operativo. Esto también está bien.
dumb_array& operator=(dumb_array&& other)
{
swap(other);
return *this;
}
Lo anterior está bien solo si dumb_array
no contiene recursos que deberían ser destruidos "inmediatamente". Por ejemplo, si el único recurso es la memoria, lo anterior está bien. Si dumb_array
podría contener bloqueos dumb_array
o el estado abierto de los archivos, el cliente podría razonablemente esperar que esos recursos en el lhs de la asignación de movimiento sean liberados inmediatamente y, por lo tanto, esta implementación podría ser problemática.
El costo del primero es dos tiendas adicionales. El costo del segundo es una prueba y rama. Ambos trabajan. Ambos cumplen con todos los requisitos de los requisitos de Table 22 MoveAssignable en el estándar C ++ 11. El tercero también funciona módulo la preocupación de recursos no de memoria.
Las tres implementaciones pueden tener diferentes costos dependiendo del hardware: ¿Qué tan caro es una rama? ¿Hay muchos registros o muy pocos?
El take-away es esa asignación de movimiento automático, a diferencia de la asignación de autoimpresión, no tiene que conservar el valor actual.
<
/ Update >
Una edición final (con suerte) inspirada en el comentario de Luc Danton:
Si está escribiendo una clase de alto nivel que no gestiona directamente la memoria (pero puede tener bases o miembros que sí la tienen), entonces la mejor implementación de la asignación de movimiento suele ser:
Class& operator=(Class&&) = default;
Esto moverá asignar cada base y cada miembro por turno, y no incluirá una this != &other
verificación. Esto le dará el rendimiento más alto y la seguridad de excepción básica suponiendo que no se deben mantener invariantes entre sus bases y miembros. Para sus clientes que exigen una fuerte excepción de seguridad, strong_assign
hacia strong_assign
.