Mover operador de asignación y `if (this! = & Rhs)`

125

En el operador de asignación de una clase, generalmente debe verificar si el objeto asignado es el objeto de invocación para no arruinar las cosas:

Class& Class::operator=(const Class& rhs) {
    if (this != &rhs) {
        // do the assignment
    }

    return *this;
}

¿Necesita lo mismo para el operador de asignación de movimiento? ¿Hay alguna situación en la this == &rhsque sería cierto?

? Class::operator=(Class&& rhs) {
    ?
}
Seth Carnegie
fuente
12
Irrelevante a la pregunta Q, y solo para que los nuevos usuarios que lean esta Q en la línea de tiempo (porque sé que Seth ya lo sabe) no tengan ideas equivocadas, Copiar e Intercambiar es la forma correcta de implementar el Operador de asignación de copias en el que Usted no es necesario verificar la autoasignación et-all.
Alok Save
55
@VaughnCato: A a; a = std::move(a);.
Xeo
11
@VaughnCato Usar std::movees normal. Luego, tenga en cuenta los alias, y cuando esté dentro de una pila de llamadas y tenga una referencia Ty otra referencia a T... ¿va a verificar la identidad aquí? ¿Desea encontrar la primera llamada (o llamadas) donde documentar que no puede pasar el mismo argumento dos veces probará estáticamente que esas dos referencias no tendrán alias? ¿O harás que la autoasignación solo funcione?
Luc Danton
2
@LucDanton Preferiría una afirmación en el operador de asignación. Si se usó std :: move de tal manera que fuera posible terminar con una autoasignación de rvalue, lo consideraría un error que debería solucionarse.
Vaughn Cato
44
@VaughnCato Un lugar en el que el intercambio automático es normal es dentro std::sorto std::shuffle, cada vez que intercambia los elementos ith y jth de una matriz sin verificar i != jprimero. ( std::swapse implementa en términos de asignación de movimiento).
Quuxplusone

Respuestas:

143

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_arrayun 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_arrayes solo otra pieza de software que tienen que reescribir porque es demasiado lento. Si se dumb_arrayhubiera 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_arraybien 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_arraylos 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:

  1. Un temporal.
  2. 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, *thisserá 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_arrayejemplo, 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 yno debe ser alterado. Cuando &x == &yentonces 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 ytenga un estado válido pero no especificado. Cuando &x == &yentonces esta condición posterior se traduce en: xtiene 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 xen cualquier estado válido pero no especificado.

Veo tres formas de programar el operador de asignación de movimiento para dumb_arraylograr 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 *thisy otherllegar a ser una matriz de tamaño cero después de la asignación de auto-movimiento, sin importar el valor original de *thises. 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_arrayno contiene recursos que deberían destruirse "inmediatamente". Por ejemplo, si el único recurso es la memoria, lo anterior está bien. Si dumb_arrayposiblemente 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 != &othercheque. 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.

Howard Hinnant
fuente
66
No sé cómo sentirme con esta respuesta. Hace que parezca que implementar esas clases (que administran su memoria de manera muy explícita) es algo común. Es cierto que cuando se hace escritura tales uno de una clase tiene que ser muy mucho cuidado con las garantías de seguridad de excepción y encontrar el punto dulce para que la interfaz sea concisa, pero conveniente, pero la cuestión parece estar pidiendo consejo general.
Luc Danton
Sí, definitivamente nunca uso copiar e intercambiar porque es una pérdida de tiempo para las clases que administran recursos y cosas (¿por qué ir y hacer otra copia completa de todos sus datos?). Y gracias, esto responde mi pregunta.
Seth Carnegie
55
Votado a favor de la sugerencia de que mover-asignación-de-sí mismo alguna vez debería afirmar-fallar o producir un resultado "no especificado". Asignación de uno mismo es, literalmente, el caso más fácil de resolver. Si su clase se bloquea std::swap(x,x), ¿por qué debería confiar en que manejará las operaciones más complicadas correctamente?
Quuxplusone
1
@Quuxplusone: Llegué a un acuerdo con usted en la afirmación-falla, como se señala en la actualización de mi respuesta. Hasta donde std::swap(x,x)llega, simplemente funciona incluso cuando x = std::move(x)produce un resultado no especificado. ¡Intentalo! No tienes que creerme.
Howard Hinnant
@HowardHinnant buen punto, swapfunciona siempre que se x = move(x)vaya xen cualquier estado de movimiento. Y los algoritmos std::copy/ std::movese definen para producir un comportamiento indefinido en las copias no operativas (¡ay, el joven de 20 años entiende memmovebien el caso trivial pero std::moveno lo hace!). Así que supongo que todavía no he pensado en un "slam dunk" para la autoasignación. Pero obviamente la autoasignación es algo que sucede mucho en el código real, ya sea que el Estándar lo haya bendecido o no.
Quuxplusone
11

Primero, se equivocó la firma del operador de asignación de movimiento. Como los movimientos roban recursos del objeto fuente, la fuente tiene que ser una constreferencia sin valor r.

Class &Class::operator=( Class &&rhs ) {
    //...
    return *this;
}

Tenga en cuenta que aún regresa a través de una referencia (no const) l -value.

Para cualquier tipo de asignación directa, el estándar no es verificar la autoasignación, sino asegurarse de que una autoasignación no provoque un choque y una quemadura. En general, nadie lo hace de forma explícita x = xo y = std::move(y)llamadas, pero aliasing, especialmente a través de múltiples funciones, puede dar lugar a = bo c = std::move(d)en ser auto-asignaciones. Una verificación explícita de la autoasignación, es decir this == &rhs, que omite la carne de la función cuando es verdadera, es una forma de garantizar la seguridad de la autoasignación. Pero es una de las peores formas, ya que optimiza un caso raro (con suerte) mientras que es una anti-optimización para el caso más común (debido a ramificaciones y posiblemente errores de caché).

Ahora, cuando (al menos) uno de los operandos es un objeto directamente temporal, nunca puede tener un escenario de autoasignación. Algunas personas abogan por asumir ese caso y optimizan tanto el código que el código se vuelve suicidamente estúpido cuando la suposición es incorrecta. Digo que descargar la verificación del mismo objeto en los usuarios es irresponsable. No hacemos ese argumento para la asignación de copias; ¿Por qué invertir la posición para la asignación de movimiento?

Hagamos un ejemplo, alterado de otro encuestado:

dumb_array& dumb_array::operator=(const dumb_array& other)
{
    if (mSize != other.mSize)
    {
        delete [] mArray;
        mArray = nullptr;  // clear this...
        mSize = 0u;        // ...and this in case the next line throws
        mArray = other.mSize ? new int[other.mSize] : nullptr;
        mSize = other.mSize;
    }
    std::copy(other.mArray, other.mArray + mSize, mArray);
    return *this;
}

Esta asignación de copias maneja la autoasignación con gracia sin una verificación explícita. Si los tamaños de origen y destino difieren, la desasignación y la reasignación preceden a la copia. De lo contrario, solo se realiza la copia. La autoasignación no obtiene una ruta optimizada, se volca en la misma ruta que cuando los tamaños de origen y destino comienzan igual. La copia es técnicamente innecesaria cuando los dos objetos son equivalentes (incluso cuando son el mismo objeto), pero ese es el precio cuando no se realiza una verificación de igualdad (en cuanto al valor o la dirección) ya que dicho cheque en sí mismo sería un desperdicio más del tiempo. Tenga en cuenta que la autoasignación de objetos aquí causará una serie de autoasignaciones a nivel de elemento; El tipo de elemento debe ser seguro para hacer esto.

Al igual que su ejemplo de origen, esta asignación de copia proporciona la garantía de seguridad de excepción básica. Si desea una garantía sólida, utilice el operador de asignación unificada de la consulta original de Copiar e Intercambiar , que maneja la asignación de copia y movimiento. Pero el objetivo de este ejemplo es reducir la seguridad en un rango para ganar velocidad. (Por cierto, estamos asumiendo que los valores de los elementos individuales son independientes; que no existe una restricción invariable que limite algunos valores en comparación con otros).

Veamos una asignación de movimiento para este mismo tipo:

class dumb_array
{
    //...
    void swap(dumb_array& other) noexcept
    {
        // Just in case we add UDT members later
        using std::swap;

        // both members are built-in types -> never throw
        swap( this->mArray, other.mArray );
        swap( this->mSize, other.mSize );
    }

    dumb_array& operator=(dumb_array&& other) noexcept
    {
        this->swap( other );
        return *this;
    }
    //...
};

void  swap( dumb_array &l, dumb_array &r ) noexcept  { l.swap( r ); }

Un tipo intercambiable que necesita personalización debe tener una función libre de dos argumentos llamada swapen el mismo espacio de nombres que el tipo. (La restricción de espacio de nombres permite que las llamadas no calificadas se intercambien para trabajar). Un tipo de contenedor también debe agregar una swapfunción de miembro público para que coincida con los contenedores estándar. Si swapno se proporciona un miembro , entonces la función libre swapprobablemente deba marcarse como amigo del tipo intercambiable. Si personaliza los movimientos para usar swap, debe proporcionar su propio código de intercambio; el código estándar llama al código de movimiento del tipo, lo que daría como resultado una recursión mutua infinita para los tipos personalizados de movimiento.

Al igual que los destructores, las funciones de intercambio y las operaciones de movimiento nunca deberían tirarse, si es posible, y probablemente marcadas como tales (en C ++ 11). Los tipos de biblioteca estándar y las rutinas tienen optimizaciones para los tipos de movimiento no tirables.

Esta primera versión de movimiento-asignación cumple el contrato básico. Los marcadores de recursos de la fuente se transfieren al objeto de destino. Los recursos antiguos no se filtrarán ya que el objeto fuente ahora los administra. Y el objeto fuente se deja en un estado utilizable donde se le pueden aplicar más operaciones, incluidas la asignación y la destrucción.

Tenga en cuenta que esta asignación de movimiento es automáticamente segura para la autoasignación, ya que la swapllamada es. También es muy seguro. El problema es la retención innecesaria de recursos. Conceptualmente, los recursos antiguos para el destino ya no son necesarios, pero aquí siguen existiendo solo para que el objeto de origen pueda permanecer válido. Si la destrucción programada del objeto fuente está muy lejos, estamos desperdiciando espacio de recursos, o peor si el espacio total de recursos es limitado y otras peticiones de recursos sucederán antes de que el (nuevo) objeto fuente muera oficialmente.

Este problema es lo que causó el controvertido consejo actual del gurú sobre el auto-objetivo durante la asignación de movimientos. La forma de escribir la asignación de movimiento sin recursos persistentes es algo como:

class dumb_array
{
    //...
    dumb_array& operator=(dumb_array&& other) noexcept
    {
        delete [] this->mArray;  // kill old resources
        this->mArray = other.mArray;
        this->mSize = other.mSize;
        other.mArray = nullptr;  // reset source
        other.mSize = 0u;
        return *this;
    }
    //...
};

La fuente se restablece a las condiciones predeterminadas, mientras que los recursos de destino antiguos se destruyen. En el caso de autoasignación, su objeto actual termina suicidándose. La forma principal de evitarlo es rodear el código de acción con un if(this != &other)bloque o atornillarlo y dejar que los clientes coman una assert(this != &other)línea inicial (si se siente bien).

Una alternativa es estudiar cómo hacer que la asignación de copias sea una excepción segura, sin asignación unificada, y aplicarla a la asignación de movimiento:

class dumb_array
{
    //...
    dumb_array& operator=(dumb_array&& other) noexcept
    {
        dumb_array  temp{ std::move(other) };

        this->swap( temp );
        return *this;
    }
    //...
};

Cuando othery thisson distintos, otherse vacía por el movimiento hacia tempy permanece así. Luego thispierde sus viejos recursos tempmientras obtiene los recursos que originalmente tenía other. Entonces los viejos recursos de thisser asesinado cuando lo temphace.

Cuando ocurre la autoasignación, el vaciado de othera también se tempvacía this. Luego, el objeto de destino recupera sus recursos cuando tempe thisintercambia. La muerte de tempreclama un objeto vacío, que debería ser prácticamente un no-op. El objeto this/ othermantiene sus recursos.

La asignación de movimiento nunca debe lanzarse, siempre que la construcción de movimiento y el intercambio también lo sean. El costo de estar a salvo también durante la autoasignación es unas pocas instrucciones más sobre los tipos de bajo nivel, que deben ser abrumados por la llamada de desasignación.

CTMacUser
fuente
¿Necesita verificar si se asignó alguna memoria antes de llamar deletea su segundo bloque de código?
user3728501
3
Su segundo ejemplo de código, el operador de asignación de copias sin verificación de autoasignación, es incorrecto. std::copyprovoca un comportamiento indefinido si los rangos de origen y destino se superponen (incluido el caso cuando coinciden). Ver C ++ 14 [alg.copy] / 3.
MM
6

Estoy en el campo de aquellos que quieren operadores seguros de autoasignación, pero no quieren escribir cheques de autoasignación en las implementaciones de operator=. Y, de hecho, ni siquiera quiero implementarlo operator=, quiero que el comportamiento predeterminado funcione 'directamente'. Los mejores miembros especiales son aquellos que vienen gratis.

Dicho esto, los requisitos de MoveAssignable presentes en el Estándar se describen a continuación (de 17.6.3.1 Requisitos de argumentos de plantilla [utility.arg.requirements], n3290):

Expresión Tipo de retorno Valor de retorno Condición posterior
t = rv T & tt es equivalente al valor de rv antes de la asignación

donde los marcadores de posición se describen como: " t[es un] valor l modificable de tipo T;" y " rves un valor de tipo T;". Tenga en cuenta que esos son requisitos establecidos en los tipos utilizados como argumentos para las plantillas de la biblioteca Estándar, pero al mirar en otra parte del Estándar, noto que cada requisito en la asignación de movimiento es similar a este.

Esto significa que a = std::move(a)tiene que ser 'seguro'. Si lo que necesita es una prueba de identidad (p this != &other. Ej. ), Hágalo, ¡de lo contrario no podrá colocar sus objetos std::vector! (A menos que no use aquellos miembros / operaciones que requieren MoveAssignable; pero no importa). Tenga en cuenta que con el ejemplo anterior a = std::move(a), entonces this == &otherse mantendrá.

Luc Danton
fuente
¿Puedes explicar cómo a = std::move(a)no trabajar haría que una clase no funcione std::vector? ¿Ejemplo?
Paul J. Lucas
@ PaulJ.Lucas No std::vector<T>::erasese permiten llamadas a menos que Tsea ​​MoveAssignable. (Como un IIRC aparte, algunos requisitos de MoveAssignable se relajaron a MoveInsertable en su lugar en C ++ 14.)
Luc Danton el
OK, entonces Tdebe ser MoveAssignable, pero ¿por qué erase()dependería alguna vez de mover un elemento a sí mismo ?
Paul J. Lucas
@ PaulJ.Lucas No hay una respuesta satisfactoria a esa pregunta. Todo se reduce a 'no romper contratos'.
Luc Danton
2

A medida que operator=se escribe su función actual , dado que ha realizado el argumento rvalue-reference const, no hay forma de que pueda "robar" los punteros y cambiar los valores de la referencia rvalue entrante ... simplemente no puede cambiarlo, usted solo podía leerlo. Solo vería un problema si comenzara a invocar deletepunteros, etc. en su thisobjeto como lo haría en un operator=método de referencia lvaue normal , pero ese tipo de derrota el punto de la versión rvalue ... es decir, sería Parece redundante usar la versión rvalue para hacer básicamente las mismas operaciones que normalmente se dejan a un método const-lvalue operator=.

Ahora, si definió su operator=para tomar una constreferencia que no sea ​​de valor, entonces la única forma en que podría ver que se requiere una verificación fue si pasó el thisobjeto a una función que devolvió intencionalmente una referencia de valor en lugar de una temporal.

Por ejemplo, supongamos que alguien intenta escribir una operator+función y utiliza una combinación de referencias de valor y referencias de valor para "evitar" que se creen temporarios adicionales durante alguna operación de suma apilada en el tipo de objeto:

struct A; //defines operator=(A&& rhs) where it will "steal" the pointers
          //of rhs and set the original pointers of rhs to NULL

A&& operator+(A& rhs, A&& lhs)
{
    //...code

    return std::move(rhs);
}

A&& operator+(A&& rhs, A&&lhs)
{
    //...code

    return std::move(rhs);
}

int main()
{
    A a;

    a = (a + A()) + A(); //calls operator=(A&&) with reference bound to a

    //...rest of code
}

Ahora, por lo que entiendo acerca de las referencias de valor, se desaconseja hacer lo anterior (es decir, debe devolver una referencia temporal, no de valor), pero, si alguien aún hiciera eso, entonces querría verificar para hacer asegúrese de que la referencia rvalue entrante no hacía referencia al mismo objeto que el thispuntero.

Jason
fuente
Tenga en cuenta que "a = std :: move (a)" es una forma trivial de tener esta situación. Sin embargo, su respuesta es válida.
Vaughn Cato
1
Totalmente de acuerdo en que es la forma más simple, aunque creo que la mayoría de la gente no lo hará intencionalmente :-) ... Tenga en cuenta que si la referencia de valor es const, entonces solo puede leer, por lo que la única necesidad es hacer una comprobación sería si decidieras operator=(const T&&)realizar la misma reinicialización de la thisque harías en un operator=(const T&)método típico en lugar de una operación de estilo de intercambio (es decir, robar punteros, etc. en lugar de hacer copias profundas).
Jason
1

Mi respuesta sigue siendo que la asignación de movimiento no tiene que salvarse contra la autoasignación, pero tiene una explicación diferente. Considere std :: unique_ptr. Si tuviera que implementar uno, haría algo como esto:

unique_ptr& operator=(unique_ptr&& x) {
  delete ptr_;
  ptr_ = x.ptr_;
  x.ptr_ = nullptr;
  return *this;
}

Si miras a Scott Meyers explicando esto, él hace algo similar. (Si vaga por qué no hacer un intercambio, tiene una escritura adicional). Y esto no es seguro para la autoasignación.

A veces esto es desafortunado. Considere salir del vector todos los números pares:

src.erase(
  std::partition_copy(src.begin(), src.end(),
                      src.begin(),
                      std::back_inserter(even),
                      [](int num) { return num % 2; }
                      ).first,
  src.end());

Esto está bien para los enteros, pero no creo que pueda hacer que algo como esto funcione con la semántica de movimiento.

Para concluir: mover la asignación al objeto en sí no está bien y hay que estar atento.

Pequeña actualización

  1. No estoy de acuerdo con Howard, lo cual es una mala idea, pero aún así, creo que la asignación automática de objetos "movidos" debería funcionar, porque swap(x, x)debería funcionar. Los algoritmos aman estas cosas! Siempre es agradable cuando un caso de esquina simplemente funciona. (Y aún no he visto un caso en el que no sea gratis. Sin embargo, eso no significa que no exista).
  2. Así es como se implementa la asignación de unique_ptrs en libc ++: unique_ptr& operator=(unique_ptr&& u) noexcept { reset(u.release()); ...} es seguro para la asignación automática.
  3. Las Pautas Básicas piensan que debería estar bien auto mover la asignación.
Denis Yaroshevskiy
fuente
0

Hay una situación en la que (esto == rhs) puedo pensar. Para esta declaración: Myclass obj; std :: move (obj) = std :: move (obj)

pequeño monstruo
fuente
Myclass obj; std :: move (obj) = std :: move (obj);
little_monster