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

c++ - Asignación de movimiento incompatible con Copia estándar e Intercambio



c++11 move-semantics (3)

Probando el nuevo Move Semantics.

Acabo de preguntar por un problema que estaba teniendo con Move Constructor. Pero como resulta en los comentarios, el problema es realmente que el operador "Mover asignación" y el operador "Asignación estándar" chocan cuando se usa el lenguaje estándar "Copiar e intercambiar".

Esta es la clase que estoy usando:

#include <string.h> #include <utility> class String { int len; char* data; public: // Default constructor // In Terms of C-String constructor String() : String("") {} // Normal constructor that takes a C-String String(char const* cString) : len(strlen(cString)) , data(new char[len+1]()) // Allocate and zero memory { memcpy(data, cString, len); } // Standard Rule of three String(String const& cpy) : len(cpy.len) , data(new char[len+1]()) { memcpy(data, cpy.data, len); } String& operator=(String rhs) { rhs.swap(*this); return *this; } ~String() { delete [] data; } // Standard Swap to facilitate rule of three void swap(String& other) throw () { std::swap(len, other.len); std::swap(data, other.data); } // New Stuff // Move Operators String(String&& rhs) throw() : len(0) , data(null) { rhs.swap(*this); } String& operator=(String&& rhs) throw() { rhs.swap(*this); return *this; } };

Bastante estándar de pantano, creo.

Entonces probé mi código así:

int main() { String a("Hi"); a = String("Test Move Assignment"); }

Aquí la asignación a debe utilizar el operador "Mover asignación". Pero hay un conflicto con el operador de "Asignación estándar" (que está escrito como su copia e intercambio estándar).

> g++ --version Configured with: --prefix=/Applications/Xcode.app/Contents/Developer/usr --with-gxx-include-dir=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.9.sdk/usr/include/c++/4.2.1 Apple LLVM version 5.0 (clang-500.2.79) (based on LLVM 3.3svn) Target: x86_64-apple-darwin13.0.0 Thread model: posix > g++ -std=c++11 String.cpp String.cpp:64:9: error: use of overloaded operator ''='' is ambiguous (with operand types ''String'' and ''String'') a = String("Test Move Assignment"); ~ ^ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ String.cpp:32:17: note: candidate function String& operator=(String rhs) ^ String.cpp:54:17: note: candidate function String& operator=(String&& rhs) ^

Ahora puedo solucionar esto modificando el operador "Asignación estándar" para:

String& operator=(String const& rhs) { String copy(rhs); copy.swap(*this); return *this; }

Pero esto no es bueno, ya que confunde con la capacidad del compilador para optimizar la copia y el intercambio. Ver ¿Qué es el lenguaje de copia e intercambio? here y here

¿Me estoy perdiendo algo no tan obvio?


Otras respuestas sugieren que solo un operator =(String rhs) sobrecarga operator =(String rhs) tome el argumento por valor, pero esta no es la implementación más eficiente.

Es cierto que en este ejemplo de David Rodríguez - dribeas.

String f(); String a; a = f(); // with String& operator=(String)

no se hace ninguna copia. Sin embargo, suponga que se proporciona solo operator =(String rhs) y considere este ejemplo:

String a("Hello"), b("World"); a = b;

Lo que ocurre es

  1. b se copia a rhs (asignación de memoria + memcpy );
  2. a y se intercambian rhs;
  3. rhs es destruido.

Si implementamos operator =(const String& rhs) y operator =(String&& rhs) , podemos evitar la asignación de memoria en el paso 1 cuando el objetivo tiene una longitud mayor que la de origen. Por ejemplo, esta es una implementación simple (no perfecta: podría ser mejor si String tuviera un miembro de capacity ):

String& operator=(const String& rhs) { if (len < rhs.len) { String tmp(rhs); swap(tmp); else { len = rhs.len; memcpy(data, rhs.data, len); data[len] = 0; } return *this; } String& operator =(String&& rhs) { swap(rhs); }

Además del punto de rendimiento si swap es noexcept , entonces operator =(String&&) puede ser noexcept . (Lo que no es el caso si la asignación de memoria se realiza "potencialmente").

Ver más detalles en esta excelente explanation por Howard Hinnant.


Si define el operador de asignación para tomar un valor, no debe (no necesita y no puede) definir el operador de asignación que toma una referencia-valor. No tiene sentido.

En general, solo necesita proporcionar una sobrecarga tomando un rvalue-reference cuando necesite diferenciar un lvalue de un rvalue, pero en este caso su elección de implementación significa que no necesita hacer esa distinción. Ya sea que tenga un lvalue o un rvalue, creará el argumento e intercambiará los contenidos.

String f(); String a; a = f(); // with String& operator=(String)

En este caso, el compilador resolverá la llamada para ser un a.operator=(f()); se dará cuenta de que la única razón para el valor de retorno es ser el argumento de operator= y se eliminará cualquier copia; ¡este es el punto de hacer que la función tome un valor en primer lugar!


Todo lo que necesitas para copiar y asignar es esto:

// As before String(const String& rhs); String(String&& rhs) : len(0), data(0) { rhs.swap(*this); } String& operator = (String rhs) { rhs.swap(*this); return *this; } void swap(String& other) noexcept { // As before }