c++ - por - language for exchange
¿Debería la Copia e Intercambio de Idioma convertirse en Copiar y Mover la Modificación en C++ 11? (3)
Como se explica en esta respuesta , la expresión copiar y cambiar se implementa de la siguiente manera:
class MyClass
{
private:
BigClass data;
UnmovableClass *dataPtr;
public:
MyClass()
: data(), dataPtr(new UnmovableClass) { }
MyClass(const MyClass& other)
: data(other.data), dataPtr(new UnmovableClass(*other.dataPtr)) { }
MyClass(MyClass&& other)
: data(std::move(other.data)), dataPtr(other.dataPtr)
{ other.dataPtr= nullptr; }
~MyClass() { delete dataPtr; }
friend void swap(MyClass& first, MyClass& second)
{
using std::swap;
swap(first.data, other.data);
swap(first.dataPtr, other.dataPtr);
}
MyClass& operator=(MyClass other)
{
swap(*this, other);
return *this;
}
};
Al tener un valor de MyClass como parámetro para operator =, el parámetro puede construirse mediante el constructor de copia o el constructor de movimiento. A continuación, puede extraer de forma segura los datos del parámetro. Esto evita la duplicación de código y ayuda a la seguridad de excepciones.
La respuesta menciona que puede intercambiar o mover las variables en el temporal. Discute principalmente el intercambio. Sin embargo, un intercambio, si no está optimizado por el compilador, involucra tres operaciones de movimiento, y en casos más complejos hace un trabajo adicional adicional. Cuando lo único que desea es mover el temporal al objeto asignado.
Considere este ejemplo más complejo, que involucra el patrón del observador . En este ejemplo, he escrito el código del operador de asignación manualmente. El énfasis está en el constructor de movimiento, el operador de asignación y el método de intercambio:
class MyClass : Observable::IObserver
{
private:
std::shared_ptr<Observable> observable;
public:
MyClass(std::shared_ptr<Observable> observable) : observable(observable){ observable->registerObserver(*this); }
MyClass(const MyClass& other) : observable(other.observable) { observable.registerObserver(*this); }
~MyClass() { if(observable != nullptr) { observable->unregisterObserver(*this); }}
MyClass(MyClass&& other) : observable(std::move(other.observable))
{
observable->unregisterObserver(other);
other.observable.reset(nullptr);
observable->registerObserver(*this);
}
friend void swap(MyClass& first, MyClass& second)
{
//Checks for nullptr and same observable omitted
using std::swap;
swap(first.observable, second.observable);
second.observable->unregisterObserver(first);
first.observable->registerObserver(first);
first.observable->unregisterObserver(second);
second.observable->registerObserver(second);
}
MyClass& operator=(MyClass other)
{
observable->unregisterObserver(*this);
observable = std::move(other.observable);
observable->unregisterObserver(other);
other.observable.reset(nullptr);
observable->registerObserver(*this);
}
}
Claramente, la parte duplicada del código en este operador de asignación escrito manualmente es idéntica a la del constructor de movimiento. Podría realizar un intercambio en el operador de asignación y el comportamiento sería correcto, pero potencialmente podría realizar más movimientos y realizar un registro adicional (en el intercambio) y anulación de registro (en el destructor).
¿No tendría mucho más sentido reutilizar el código del constructor de movimientos en lugar?
private:
void performMoveActions(MyClass&& other)
{
observable->unregisterObserver(other);
other.observable.reset(nullptr);
observable->registerObserver(*this);
}
public:
MyClass(MyClass&& other) : observable(std::move(other.observable))
{
performMoveActions(other);
}
MyClass& operator=(MyClass other)
{
observable->unregisterObserver(*this);
observable = std::move(other.observable);
performMoveActions(other);
}
Me parece que este enfoque nunca es inferior al enfoque de intercambio. ¿Estoy en lo cierto al pensar que la expresión copiar y intercambiar sería mejor que la expresión copiar y mover en C ++ 11, o me perdí algo importante?
Bríndele a cada miembro especial el cuidado amoroso y tierno que merece, y trate de incumplirlos tanto como sea posible:
class MyClass
{
private:
BigClass data;
std::unique_ptr<UnmovableClass> dataPtr;
public:
MyClass() = default;
~MyClass() = default;
MyClass(const MyClass& other)
: data(other.data)
, dataPtr(other.dataPtr ? new UnmovableClass(*other.dataPtr)
: nullptr)
{ }
MyClass& operator=(const MyClass& other)
{
if (this != &other)
{
data = other.data;
dataPtr.reset(other.dataPtr ? new UnmovableClass(*other.dataPtr)
: nullptr);
}
return *this;
}
MyClass(MyClass&&) = default;
MyClass& operator=(MyClass&&) = default;
friend void swap(MyClass& first, MyClass& second)
{
using std::swap;
swap(first.data, second.data);
swap(first.dataPtr, second.dataPtr);
}
};
El destructor podría estar implícitamente predeterminado de manera predeterminada si así lo desea. Todo lo demás debe ser explícitamente definido o predeterminado para este ejemplo.
Referencia: http://accu.org/content/conf2014/Howard_Hinnant_Accu_2014.pdf
La expresión de copia / intercambio probablemente le cueste rendimiento (vea las diapositivas). Por ejemplo, alguna vez se preguntan por qué std :: tipos de alto rendimiento / uso frecuente como std::vector
y std::string
no usan copy / swap? El mal desempeño es la razón. Si BigClass
contiene cualquier std::vector
s o std::string
s (lo que parece probable), su mejor opción es llamar a sus miembros especiales de sus miembros especiales. Lo anterior es cómo hacer eso.
Si necesita una fuerte excepción de seguridad en la tarea, vea las diapositivas sobre cómo ofrecer eso además del rendimiento (busque "strong_assign").
En primer lugar, generalmente no es necesario escribir una función de swap
en C ++ 11 siempre que su clase sea móvil. El swap
predeterminado recurrirá a movimientos:
void swap(T& left, T& right) {
T tmp(std::move(left));
left = std::move(right);
right = std::move(tmp);
}
Y eso es todo, los elementos se intercambian.
En segundo lugar, en base a esto, el Copiar-e-Cambiar en realidad todavía tiene:
T& T::operator=(T const& left) {
using std::swap;
T tmp(left);
swap(*this, tmp);
return *this;
}
// Let''s not forget the move-assignment operator to power down the swap.
T& T::operator=(T&&) = default;
Copiará e intercambiará (lo cual es un movimiento) o moverá y intercambiará (lo cual es un movimiento), y siempre debe lograr cerca del rendimiento óptimo. Puede haber un par de asignaciones redundantes, pero es de esperar que su compilador se encargue de ello.
EDITAR: esto solo implementa el operador de copia-asignación; también se requiere un operador de asignación de movimiento separado, aunque puede ser predeterminado, de lo contrario se producirá un desbordamiento de la pila (mover-asignar e intercambiar llamadas entre sí indefinidamente).
Ha pasado mucho tiempo desde que hice esta pregunta, y he sabido la respuesta desde hace un tiempo, pero he postergado la redacción de la respuesta. Aquí está.
La respuesta es no. La expresión copiar y cambiar no debe convertirse en la expresión copiar y mover.
Una parte importante de Copy-and-swap (que también es Move-construct-and-swap) es una forma de implementar operadores de asignación con una limpieza segura. Los datos antiguos se intercambian en una copia temporal construida o construida en movimiento. Cuando la operación finaliza, se elimina el temporal y se llama a su destructor.
El comportamiento de intercambio está ahí para poder reutilizar el destructor, por lo que no tiene que escribir ningún código de limpieza en los operadores de asignación.
Si no hay un comportamiento de limpieza y solo una asignación, entonces usted debe poder declarar los operadores de asignación como predeterminados y no es necesario copiar y cambiar.
El constructor de movimientos en sí mismo no requiere ningún tipo de comportamiento de limpieza, ya que es un objeto nuevo. El enfoque simple general es hacer que el constructor de movimientos invoque el constructor predeterminado, y luego intercambie todos los miembros con el objeto move-from. El objeto movido desde allí será como un objeto blando predeterminado construido.
Sin embargo, en el ejemplo del patrón de observadores de esta pregunta, en realidad es una excepción en la que tiene que hacer un trabajo de limpieza adicional porque las referencias al objeto antiguo deben modificarse. En general, recomendaría que sus observadores y observables, y otras construcciones de diseño basadas en referencias, sean inamovibles siempre que sea posible.