c++ c++11 move-semantics copy-and-swap

c++ - ¿Copiar y mover idioma?



c++11 move-semantics (2)

Al utilizar el lenguaje Copiar e Intercambiar , podemos implementar fácilmente la asignación de copias con una fuerte excepción de seguridad:

T& operator = (T other){ using std::swap; swap(*this, other); return *this; }

Sin embargo, esto requiere que T sea Swappable . Que tipo es automáticamente si std::is_move_constructible_v<T> && std::is_move_assignable_v<T> == true gracias a std::swap .

Mi pregunta es, ¿hay algún inconveniente en usar un modificador "Copiar y mover" en su lugar? Al igual que:

T& operator = (T other){ *this = std::move(other); return *this; }

siempre que implementes la asignación de movimientos para T porque, obviamente, terminas con una recursión infinita de lo contrario.

Esta pregunta es diferente de si el lenguaje de copiar e intercambiar se convierte en el idioma de copiar y mover en C ++ 11. ya que esta pregunta es más general y utiliza el operador de asignación de movimiento en lugar de mover los miembros manualmente. Lo que evita los problemas de limpieza que predijeron la respuesta en el hilo vinculado.


Mi pregunta es, ¿hay algún inconveniente en usar un modificador "Copiar y mover" en su lugar?

Sí, se obtiene un desbordamiento de pila si no implementa el operator =(T&&) asignación de movimiento operator =(T&&) . Si desea implementar que obtiene un error de compilador ( ejemplo aquí ):

struct test { test() = default; test(const test &) = default; test & operator = (test t) { (*this) = std::move(t); return (*this); } test & operator = (test &&) { return (*this); } };

y si haces la test a,b; a = b; test a,b; a = b; te sale el error:

error: ambiguous overload for ''operator='' (operand types are ''test'' and ''std::remove_reference<test&>::type {aka test}'')

Una forma de resolver esto es usar un constructor de copia:

test & operator = (const test& t) { *this = std::move(test(t)); return *this; }

Esto funcionará, sin embargo, si no implementa la asignación de movimiento, es posible que no reciba un error (según la configuración del compilador). Teniendo en cuenta el error humano, es posible que este caso pueda suceder y termine el desbordamiento de la pila en el tiempo de ejecución, lo que es malo.


Corrección a la pregunta.

La forma de implementar Copy & Move tiene que ser como @Raxvan señaló:

T& operator=(const T& other){ *this = T(other); return *this; }

pero sin el std::move como T(other) ya es un valor y clang emitirá una advertencia sobre pesimismo cuando se usa std::move aquí.

Resumen

Cuando existe un operador de asignación de movimiento, la diferencia entre Copiar y cambiar y Copiar y mover depende de si el usuario está utilizando un método de swap que tenga una mejor seguridad de excepción que la asignación de movimiento. Para el estándar std::swap la excepción de seguridad es idéntica entre Copy & Swap y Copy & Move. Creo que la mayoría de las veces, el swap y la asignación de movimiento tendrán la misma excepción de seguridad (pero no siempre).

Implementar Copy & Move tiene un riesgo en el que si el operador de asignación de movimiento no está presente o tiene la firma incorrecta, el operador de asignación de copia se reducirá a una recursión infinita. Sin embargo, al menos Clang advierte sobre esto y al pasar -Werror=infinite-recursion al compilador, este miedo puede eliminarse, lo que francamente me supera por qué no es un error por defecto, pero estoy divagando.

Motivación

He hecho algunas pruebas y mucho rascarme la cabeza y esto es lo que he descubierto:

  1. Si tiene un operador de asignación de movimiento, la forma "correcta" de hacer Copiar e Intercambiar no funcionará debido a que la llamada al operator=(T) es ambigua con el operator=(T&&) . Como señaló @Raxvan, debe hacer la construcción de copia dentro del cuerpo del operador de asignación de copia. Esto se considera inferior, ya que evita que el compilador realice una elision de copia cuando se llama al operador con un valor de r. Sin embargo, los casos en los que se habría aplicado elision de copia son manejados por la asignación de movimiento ahora, de modo que el punto es discutible.

  2. Tenemos que comparar:

    T& operator=(const T& other){ using std::swap; swap(*this, T(other)); return *this; }

    a:

    T& operator=(const T& other){ *this = T(other); return *this; }

    Si el usuario no está usando un swap personalizado, entonces se usa el std::swap(a,b) plantilla. Lo que esencialmente hace esto:

    template<typename T> void swap(T& a, T& b){ T c(std::move(a)); a = std::move(b); b = std::move(c); }

    Lo que significa que la seguridad de excepción de Copia y Swap es la misma seguridad de excepción que la construcción de movimiento y la asignación de movimiento más débiles. Si el usuario está usando un intercambio personalizado, entonces, por supuesto, la función de intercambio dicta la seguridad de la excepción.

    En Copiar y mover, la excepción de seguridad es dictada completamente por el operador de asignación de movimiento.

    Creo que considerar el rendimiento aquí es un tanto discutible, ya que las optimizaciones del compilador probablemente no harán ninguna diferencia en la mayoría de los casos. Pero lo comentaré de todos modos: la copia y el intercambio realizan una construcción de copia, una construcción de movimiento y dos asignaciones de movimiento, en comparación con Copiar y mover que hace una construcción de copia y solo una asignación de movimiento. Aunque estoy esperando que el compilador genere el mismo código de máquina en la mayoría de los casos, por supuesto, dependiendo de T.

Anexo: El código que utilicé.

class T { public: T() = default; T(const std::string& n) : name(n) {} T(const T& other) = default; #if 0 // Normal Copy & Swap. // // Requires this to be Swappable and copy constructible. // // Strong exception safety if `std::is_nothrow_swappable_v<T> == true` or user provided // swap has strong exception safety. Note that if `std::is_nothrow_move_assignable` and // `std::is_nothrow_move_constructible` are both true, then `std::is_nothrow_swappable` // is also true but it does not hold that if either of the above are true that T is not // nothrow swappable as the user may have provided a specialized swap. // // Doesn''t work in presence of a move assignment operator as T t1 = std::move(t2) becomes // ambiguous. T& operator=(T other) { using std::swap; swap(*this, other); return *this; } #endif #if 0 // Copy & Swap in presence of copy-assignment. // // Requries this to be Swappable and copy constructible. // // Same exception safety as the normal Copy & Swap. // // Usually considered inferor to normal Copy & Swap as the compiler now cannot perform // copy elision when called with an rvalue. However in the presence of a move assignment // this is moot as any rvalue will bind to the move-assignment instead. T& operator=(const T& other) { using std::swap; swap(*this, T(other)); return *this; } #endif #if 1 // Copy & Move // // Requires move-assignment to be implemented and this to be copy constructible. // // Exception safety, same as move assignment operator. // // If move assignment is not implemented, the assignment to this in the body // will bind to this function and an infinite recursion will follow. T& operator=(const T& other) { // Clang emits the following if a user or default defined move operator is not present. // > "warning: all paths through this function will call itself [-Winfinite-recursion]" // I recommend "-Werror=infinite-recursion" or "-Werror" compiler flags to turn this into an // error. // This assert will not protect against missing move-assignment operator. static_assert(std::is_move_assignable<T>::value, "Must be move assignable!"); // Note that the following will cause clang to emit: // warning: moving a temporary object prevents copy elision [-Wpessimizing-move] // *this = std::move(T{other}); // The move doesn''t do anything anyway so write it like this; *this = T(other); return *this; } #endif #if 1 T& operator=(T&& other) { // This will cause infinite loop if user defined swap is not defined or findable by ADL // as the templated std::swap will use move assignment. // using std::swap; // swap(*this, other); name = std::move(other.name); return *this; } #endif private: std::string name; };