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 se dumb_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
:
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 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 un memcpy
(oculto debajo 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 seguridad de excepción fuerte:
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 dumb_array
los clientes valoran la velocidad, deben llamar al operator=
. 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):
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 no, no necesita este cheque.
Razón fundamental:
Cuando un objeto se une a una referencia de valor r es una de dos cosas:
- Un temporal.
- Un objeto que la persona que llama quiere que creas es temporal.
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:
Class&
Class::operator=(Class&& other)
{
assert(this != &other);
// ...
return *this;
}
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
:
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, por delete [] 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 nuestro dumb_array
ejemplo, esto será perfectamente inofensivo si simplemente omitimos la afirmación o la restringimos al caso de mudado:
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 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:
x = y;
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 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
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 permitir swap(x, x)
simplemente trabajar:
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 y cuando x = std::move(x)
no se bloquee. Puede salir x
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:
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 asignación de uno mismo, pero *this
y other
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.
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 hacer que no funcione. 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 destruirse "inmediatamente". Por ejemplo, si el único recurso es la memoria, lo anterior está bien. Si dumb_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:
Class& operator=(Class&&) = default;
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úntelos strong_assign
.