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
-
b
se copia arhs
(asignación de memoria +memcpy
); -
a
y se intercambian rhs; -
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
}