Frage

Wie in erklärt diese Antwort, das Copy-and-Swap-Idiom wird wie folgt implementiert:

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;
    }
};

Durch den Wert „MyClass“ als Parameter für „operator=“ kann der Parameter entweder durch den Kopierkonstruktor oder den Verschiebungskonstruktor erstellt werden.Anschließend können Sie die Daten sicher aus dem Parameter extrahieren.Dies verhindert Codeduplizierung und trägt zur Ausnahmesicherheit bei.

In der Antwort wird erwähnt, dass Sie die Variablen im Temporären entweder austauschen oder verschieben können.Dabei geht es in erster Linie um den Tausch.Allerdings umfasst ein Swap, wenn er nicht vom Compiler optimiert wird, drei Verschiebungsvorgänge und verursacht in komplexeren Fällen zusätzliche zusätzliche Arbeit.Wenn Sie nur wollen bewegen das Temporäre in das zugewiesene Objekt.

Betrachten Sie dieses komplexere Beispiel mit dem Beobachtermuster.In diesem Beispiel habe ich den Zuweisungsoperatorcode manuell geschrieben.Der Schwerpunkt liegt auf dem Move-Konstruktor, dem Zuweisungsoperator und der Swap-Methode:

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);
    }
}

Offensichtlich ist der duplizierte Teil des Codes in diesem manuell geschriebenen Zuweisungsoperator identisch mit dem des Verschiebungskonstruktors.Sie könnten einen Austausch im Zuweisungsoperator durchführen und das Verhalten wäre richtig, aber es würde möglicherweise mehr Bewegungen ausführen und eine zusätzliche Registrierung (im Austausch) und eine Aufhebung der Registrierung (im Destruktor) durchführen.

Wäre es nicht viel sinnvoller, stattdessen den Code des Move-Konstruktors wiederzuverwenden?

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);
    }

Meiner Meinung nach steht dieser Ansatz dem Swap-Ansatz in nichts nach.Liege ich richtig, wenn ich denke, dass das Copy-and-Swap-Idiom besser geeignet wäre als das Copy-and-Move-Idiom in C++11, oder habe ich etwas Wichtiges übersehen?

War es hilfreich?

Lösung 3

Es ist schon lange her, seit ich diese Frage gestellt habe, und ich habe die Antwort jetzt schon eine Weile gekannt, aber ich habe die Antwort dafür abgesetzt. Hier ist es.

Die Antwort ist nein. Der Kopier- und Swap-IDIOM sollte nicht zum Kopieren-and-Move-IDIOM werden.

Ein wichtiger Teil von Copy-and-Swap (was auch Move-construct-and-swap) ist ein Weg, um Zuweisungsoperatoren mit sicherer Bereinigung umzusetzen. Die alten Daten werden in eine kopiergebaute oder bewegte oder bewegte vorübergehende vorübergehende temporäre Teile ausgetauscht. Wenn der Betrieb durchgeführt wird, wird das Temporäre gelöscht, und sein Zerstörer wird aufgerufen.

Das Swap-Verhalten ist da, um den Destruktor wiederverwenden zu können, sodass Sie keinen Bereinigungscode in Ihre Auftragsbetreiber schreiben müssen.

Wenn es kein Cleanup-Verhalten gibt, und nur die Zuweisung, dann sollten Sie in der Lage sein, die Auftragsbetreiber als Standard- und Copy-and-Swap nicht erforderlich zu erklären.

Der MOVE-Konstruktor selbst erfordert normalerweise kein Reinigungsverhalten, da er ein neues Objekt ist. Der allgemeine einfache Ansatz besteht darin, den Konstruktor des Move-Konstruktors den Standardkonstruktor aufzurufen, und tauschen Sie alle Mitglieder mit dem MOVE-vom Objekt aus. Das umgefahrene Objekt ist dann wie ein hochfestes Standard-konstruktives Objekt.

In diesem Beispiel der Beobachtermuster-Beispiele ist dies jedoch eigentlich eine Ausnahme, in der Sie zusätzliche Bereinigungsarbeiten durchführen müssen, da Hinweise auf das alte Objekt geändert werden müssen. Im Allgemeinen würde ich empfehlen, Ihre Beobachter und Beachtlungen zu machen, und andere Designkonstrukte, die sich auf der Grundlage von Referenzen basieren, unmissverlässig, wann immer dies möglich ist.

Andere Tipps

Geben Sie jedem besonderen Mitglied die zärtliche, liebevolle Fürsorge, die es verdient, und versuchen Sie, es so weit wie möglich in Verzug zu bringen:

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);
    }
};

Der Destruktor könnte bei Bedarf implizit oben voreingestellt werden.Alles andere muss für dieses Beispiel explizit definiert oder voreingestellt werden.

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

Die Kopier-/Austausch-Sprache kostet Sie wahrscheinlich Leistung (siehe Folien).Fragen Sie sich zum Beispiel jemals, warum hohe Leistung / häufig verwendete std::types so sind std::vector Und std::string Kein Kopieren/Austauschen verwenden?Schlechte Leistung ist der Grund.Wenn BigClass enthält irgendwelche std::vectors oder std::strings (was wahrscheinlich erscheint), Ihre beste Wahl ist es, ihre Sondermitglieder von Ihren Sondermitgliedern anzurufen.Wie das geht, erfahren Sie oben.

Wenn Sie für die Zuweisung eine starke Ausnahmesicherheit benötigen, erfahren Sie auf den Folien, wie Sie diese zusätzlich zur Leistung anbieten können (suchen Sie nach „strong_assign“).

Zunächst einmal ist es im Allgemeinen nicht erforderlich, eine zu schreiben swap Funktion in C++11, solange Ihre Klasse verschiebbar ist.Der Standard swap wird auf Umzüge zurückgreifen:

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

Und das war's, die Elemente werden vertauscht.

Zweitens gilt auf dieser Grundlage tatsächlich immer noch das Copy-And-Swap-Prinzip:

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;

Wird entweder kopieren und tauschen (was eine Bewegung ist) oder verschieben und tauschen (was eine Bewegung ist) und sollte immer eine nahezu optimale Leistung erzielen.Möglicherweise gibt es ein paar redundante Zuweisungen, aber hoffentlich kümmert sich Ihr Compiler darum.

BEARBEITEN: Dadurch wird nur der Kopierzuweisungsoperator implementiert.Es ist auch ein separater Move-Assignment-Operator erforderlich, der jedoch standardmäßig verwendet werden kann, andernfalls kommt es zu einem Stapelüberlauf (Move-Assignment und Swap rufen sich gegenseitig auf unbestimmte Zeit auf).

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top