Aquí, tengo un programa muy simple que mueve un valor de un objeto a otro, asegurándose de eliminar el valor del que fue tomado (dejando un '0').

#include <iostream>

struct S
{
    S(char val) : m_val(val) {}
    S& operator=(S&& other) noexcept
    {
        this->m_val = other.m_val;
        other.m_val = '0';

        return *this;
    }
    char m_val = '0';
};

int main()
{
    S a('a');
    S b('b');

    std::cout << "a.m_val = '" << a.m_val << "'" << std::endl;
    std::cout << "b.m_val = '" << b.m_val << "'" << std::endl;

    a = std::move(b);

    std::cout << "a.m_val = '" << a.m_val << "'" << std::endl;
    std::cout << "b.m_val = '" << b.m_val << "'" << std::endl;

    return 0;
}

Como se esperaba, el resultado de este programa es:

a.m_val = 'a'
b.m_val = 'b'
a.m_val = 'b'
b.m_val = '0'

El valor de 'b' se transfiere del objeto b al objeto a, dejando atrás un '0'. Ahora, si generalizo esto un poco más con una plantilla para (con suerte) hacer el movimiento y eliminar negocios automáticamente, esto es lo que termino con ... (destilado, por supuesto).

#include <iostream>

template<typename T>
struct P
{
    P<T>& operator=(P<T>&& other) noexcept
    {
        T& thisDerived = static_cast<T&>(*this);
        T& otherDerived = static_cast<T&>(other);

        thisDerived = otherDerived;
        otherDerived.m_val = '0';

        return *this;
    }
protected:
    P<T>& operator=(const P<T>& other) = default;
};

struct S : public P<S>
{
    S(char val) : m_val(val) {}

    char m_val = '0';
};

int main()
{
    S a('a');
    S b('b');

    std::cout << "a.m_val = '" << a.m_val << "'" << std::endl;
    std::cout << "b.m_val = '" << b.m_val << "'" << std::endl;

    a = std::move(b);

    std::cout << "a.m_val = '" << a.m_val << "'" << std::endl;
    std::cout << "b.m_val = '" << b.m_val << "'" << std::endl;

    return 0;
}

Cuando se ejecuta, la salida es:

a.m_val = 'a'
b.m_val = 'b'
a.m_val = '0'
b.m_val = '0'

¡UH oh! De alguna manera AMBOS objetos fueron "eliminados". Cuando paso por el cuerpo del código del operador de asignación de movimiento ... ¡todo parece estar bien! a.m_val es 'b' como esperamos ... hasta la declaración return *this;. Una vez que vuelve de la función, de repente ese valor se vuelve a establecer en '0'. ¿Alguien puede arrojar algo de luz sobre por qué sucede esto?

2
BeigeAlert 28 feb. 2020 a las 06:08

2 respuestas

La mejor respuesta
P<T>& operator=(P<T>&& other) noexcept

Este es un operador de asignación de movimiento explícito para esta clase de plantilla.

struct S : public P<S> {

Esta subclase hereda de esta clase de plantilla. P<S> es su clase padre.

Esta subclase no tiene un operador de asignación de movimiento explícito, por lo que su compilador de C ++ crea útilmente un operador de asignación de movimiento predeterminado para usted, porque así es como funciona C ++. El operador de asignación de movimiento predeterminado invoca al operador de asignación de movimiento de la clase principal, y el operador de asignación de movimiento predeterminado luego asigna a todos los miembros de esta clase.

El hecho de que la clase principal tenga un operador de asignación de movimiento explícito (su operador de asignación de movimiento) no hace que este el operador de asignación de movimiento predeterminado de la clase secundaria desaparezca. El operador de asignación de movimiento predeterminado de S es efectivamente esto, very hablando en términos generales:

S &operator=(S &&other)
{
    P<S>::operator=(std::move(other));
    this->m_val=std::move(other.m_val);

    return *this;
}

Eso es lo que obtienes gratis, desde tu compilador de C ++. ¿No es bueno que su compilador de C ++ proporcione un operador de asignación de movimiento predeterminado tan útil para su clase?

a = std::move(b);

En realidad, esto termina invocando el operador de asignación de movimiento predeterminado anterior.

Que primero invoca el operador de asignación de movimiento de la clase principal, el que usted escribió.

Lo que efectivamente establece other.m_val en '0'.

Y cuando vuelve, este operador de asignación de movimiento predeterminado también establece this->m_val en '0'.

3
Sam Varshavchik 28 feb. 2020 a las 03:25

El problema es que S tiene un operador de asignación de movimiento generado implícitamente, que llama al operador de asignación de movimiento de la clase base (es decir, P<T>::operator=), luego realiza la asignación de movimiento en función de los miembros (es decir, S::m_val). En P<T>::operator=, other.m_val ha sido asignado a '0', luego other.m_val this->m_val lo asigna a other.m_val y se convierte en {{X9} } también.

Puede definir un operador de asignación de movimiento definido por el usuario para S y no espera llamar a la versión de clase base. p.ej.

struct S : public P<S>
{
    S(char val) : m_val(val) {}

    char m_val = '0';

    S& operator=(S&& other) {
        P<S>::operator=(other);
        return *this;
    }
};

VIVA

3
songyuanyao 28 feb. 2020 a las 03:27