Pregunta

Como se explica en esta respuesta, el modismo copiar e intercambiar se implementa de la siguiente manera:

class MyClass
{
private:
    BigClass data;
    UnmovableClass *dataPtr;

public:
    MyClass()
      : data(), dataPtr(new UnmovableClass) { }
    MyClass(const MyClass& other)
      : data(other.data), dataPtr(new UnmovableClass(*other.dataPtr)) { }
    MyClass(MyClass&& other)
      : data(std::move(other.data)), dataPtr(other.dataPtr)
    { other.dataPtr= nullptr; }

    ~MyClass() { delete dataPtr; }

    friend void swap(MyClass& first, MyClass& second)
    {
        using std::swap;
        swap(first.data, other.data);
        swap(first.dataPtr, other.dataPtr);
    }

    MyClass& operator=(MyClass other)
    {
        swap(*this, other);
        return *this;
    }
};

Al tener un valor de MyClass como parámetro para operator=, el parámetro puede ser construido por el constructor de copia o por el constructor de movimiento.Luego podrá extraer de forma segura los datos del parámetro.Esto evita la duplicación de código y ayuda en la seguridad de excepciones.

La respuesta menciona que puede intercambiar o mover las variables en el temporal.Se trata principalmente del intercambio.Sin embargo, un intercambio, si el compilador no lo optimiza, implica tres operaciones de movimiento y, en casos más complejos, supone un trabajo adicional.Cuando lo único que quieres es mover el temporal en el objeto asignado.

Consideremos este ejemplo más complejo, que involucra la patrón de observador.En este ejemplo, escribí el código del operador de asignación manualmente.Se hace hincapié en el constructor de movimiento, el operador de asignación y el método de intercambio:

class MyClass : Observable::IObserver
{
private:
    std::shared_ptr<Observable> observable;

public:
    MyClass(std::shared_ptr<Observable> observable) : observable(observable){ observable->registerObserver(*this); }
    MyClass(const MyClass& other) : observable(other.observable) { observable.registerObserver(*this); }
    ~MyClass() { if(observable != nullptr) { observable->unregisterObserver(*this); }}

    MyClass(MyClass&& other) : observable(std::move(other.observable))
    {
        observable->unregisterObserver(other);
        other.observable.reset(nullptr);
        observable->registerObserver(*this);
    }

    friend void swap(MyClass& first, MyClass& second)
    {
        //Checks for nullptr and same observable omitted
            using std::swap;
            swap(first.observable, second.observable);

            second.observable->unregisterObserver(first);
            first.observable->registerObserver(first);
            first.observable->unregisterObserver(second);
            second.observable->registerObserver(second);
    }

    MyClass& operator=(MyClass other)
    {
        observable->unregisterObserver(*this);
        observable = std::move(other.observable);

        observable->unregisterObserver(other);
        other.observable.reset(nullptr);
        observable->registerObserver(*this);
    }
}

Claramente, la parte duplicada del código en este operador de asignación escrito manualmente es idéntica a la del constructor de movimientos.Podría realizar un intercambio en el operador de asignación y el comportamiento sería correcto, pero potencialmente realizaría más movimientos y realizaría un registro adicional (en el intercambio) y una cancelación del registro (en el destructor).

¿No tendría mucho más sentido reutilizar el código del constructor de movimientos?

private:
    void performMoveActions(MyClass&& other)
    {
        observable->unregisterObserver(other);
        other.observable.reset(nullptr);
        observable->registerObserver(*this);
    }

public:
    MyClass(MyClass&& other) : observable(std::move(other.observable))
    {
        performMoveActions(other);
    }

    MyClass& operator=(MyClass other)
    {
        observable->unregisterObserver(*this);
        observable = std::move(other.observable);

        performMoveActions(other);
    }

Me parece que este enfoque nunca es inferior al enfoque de intercambio.¿Estoy en lo cierto al pensar que el modismo copiar e intercambiar sería mejor que el modismo copiar y mover en C++ 11, o me perdí algo importante?

¿Fue útil?

Solución 3

Ha pasado mucho tiempo desde que hice esta pregunta, y he conocido la respuesta por un tiempo ahora, pero he dejado escribir la respuesta para ello. Aquí está.

La respuesta es no. El idioma de copia y swap no debe convertirse en el idioma de copia y movimiento.

Una parte importante de la copia y el intercambio (que también es Mover-Construct-and-swap) es una forma de implementar operadores de asignación con una limpieza segura. Los datos antiguos se intercambian en un temporal construido o construido a copia. Cuando se realiza la operación, se elimina el temporal y se llama su destructor.

El comportamiento de intercambio está allí para poder reutilizar al destructor, por lo que no tiene que escribir ningún código de limpieza en sus operadores de asignación.

Si no hay un comportamiento de limpieza que debe hacerse y solo la asignación, debe poder declarar los operadores de asignación como predeterminado y no se necesita copia y swap.

El constructor de movimiento en sí generalmente no requiere ningún comportamiento de limpieza, ya que es un nuevo objeto. El enfoque general simple es hacer que el constructor de movimiento invoque el constructor predeterminado, y luego cambie a todos los miembros con el objeto Move-On. El objeto movido desde entonces será como un objeto incumplido con incumplimiento.

Sin embargo, en el ejemplo del patrón de observador de esta pregunta, esa es una excepción en la que tiene que hacer un trabajo de limpieza adicional porque las referencias al objeto anterior deben cambiarse. En general, recomendaría hacer que sus observadores y observables, y otras construcciones de diseño basadas en referencias, inaptables siempre que sea posible.

Otros consejos

Brinde a cada miembro especial el cuidado tierno y amoroso que se merece y trate de ignorarlo tanto como sea posible:

class MyClass
{
private:
    BigClass data;
    std::unique_ptr<UnmovableClass> dataPtr;

public:
    MyClass() = default;
    ~MyClass() = default;
    MyClass(const MyClass& other)
        : data(other.data)
        , dataPtr(other.dataPtr ? new UnmovableClass(*other.dataPtr)
                                : nullptr)
        { }
    MyClass& operator=(const MyClass& other)
    {
        if (this != &other)
        {
            data = other.data;
            dataPtr.reset(other.dataPtr ? new UnmovableClass(*other.dataPtr)
                                        : nullptr);
        }
        return *this;
    }
    MyClass(MyClass&&) = default;
    MyClass& operator=(MyClass&&) = default;

    friend void swap(MyClass& first, MyClass& second)
    {
        using std::swap;
        swap(first.data, second.data);
        swap(first.dataPtr, second.dataPtr);
    }
};

El destructor podría estar implícitamente predeterminado arriba si se desea.Todo lo demás debe definirse explícitamente o establecerse de forma predeterminada para este ejemplo.

Referencia: http://accu.org/content/conf2014/Howard_Hinnant_Accu_2014.pdf

El modismo copiar/intercambiar probablemente le costará rendimiento (consulte las diapositivas).Por ejemplo, ¿alguna vez se ha preguntado por qué std::types de alto rendimiento/uso frecuente como std::vector y std::string ¿No usas copiar/intercambiar?El mal desempeño es la razón.Si BigClass contiene cualquier std::vectors o std::strings (lo que parece probable), lo mejor que puede hacer es llamar a sus miembros especiales desde sus miembros especiales.Lo anterior es cómo hacer eso.

Si necesita una seguridad de excepción sólida en la tarea, consulte las diapositivas sobre cómo ofrecerla además del rendimiento (busque "strong_assign").

En primer lugar, generalmente no es necesario escribir un swap funciona en C++ 11 siempre que su clase sea móvil.El valor por defecto swap recurrirá a movimientos:

void swap(T& left, T& right) {
    T tmp(std::move(left));
    left = std::move(right);
    right = std::move(tmp);
}

Y listo, se intercambian los elementos.

En segundo lugar, en base a esto, Copiar e intercambiar en realidad todavía se mantiene:

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

// Let's not forget the move-assignment operator to power down the swap.
T& T::operator=(T&&) = default;

Copiará e intercambiará (que es un movimiento) o moverá e intercambiará (que es un movimiento), y siempre debe lograr un rendimiento cercano al óptimo.Puede que haya un par de tareas redundantes, pero es de esperar que su compilador se encargue de ello.

EDITAR: esto sólo implementa el operador de asignación de copia;También se requiere un operador de asignación de movimiento separado, aunque puede ser predeterminado; de lo contrario, se producirá un desbordamiento de la pila (la asignación de movimiento y el intercambio se llaman entre sí indefinidamente).

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top