Verschieben Sie den Zuweisungsoperator und `if (this! = & Rhs)`

125

Im Zuweisungsoperator einer Klasse müssen Sie normalerweise überprüfen, ob das zugewiesene Objekt das aufrufende Objekt ist, damit Sie nichts vermasseln:

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

    return *this;
}

Benötigen Sie dasselbe für den Verschiebungszuweisungsoperator? Gibt es jemals eine Situation, in this == &rhsder dies wahr wäre?

? Class::operator=(Class&& rhs) {
    ?
}
Seth Carnegie
quelle
12
Unabhängig davon, ob das Q gefragt wird und nur damit neue Benutzer, die dieses Q in der Zeitleiste lesen (ich weiß, dass Seth dies bereits weiß), keine falschen Ideen bekommen, ist Copy and Swap der richtige Weg, um den Operator für die Kopierzuweisung zu implementieren, in dem Sie Sie müssen nicht nach Selbstzuweisung suchen.
Alok Save
5
@VaughnCato : A a; a = std::move(a);.
Xeo
11
@VaughnCato Verwendung std::moveist normal. Berücksichtigen Sie dann das Aliasing. Wenn Sie sich tief in einem Aufrufstapel befinden und einen Verweis auf Tund einen weiteren Verweis auf T... haben, werden Sie hier nach der Identität suchen? Möchten Sie den ersten Aufruf (oder die ersten Aufrufe) finden, bei denen die Dokumentation, dass Sie nicht dasselbe Argument zweimal übergeben können, statisch beweist, dass diese beiden Verweise keinen Alias ​​darstellen? Oder werden Sie dafür sorgen, dass die Selbstzuweisung einfach funktioniert?
Luc Danton
2
@LucDanton Ich würde eine Behauptung im Zuweisungsoperator bevorzugen. Wenn std :: move so verwendet würde, dass es möglich wäre, eine rvalue-Selbstzuweisung zu erhalten, würde ich dies als einen Fehler betrachten, der behoben werden sollte.
Vaughn Cato
4
@VaughnCato Ein Ort, an dem der Selbsttausch normal ist, befindet sich entweder innerhalb std::sortoder std::shuffle- jedes Mal, wenn Sie das idritte und das jdritte Element eines Arrays austauschen, ohne dies i != jvorher zu überprüfen . ( std::swapwird in Bezug auf die
Zugzuweisung

Antworten:

142

Wow, hier gibt es einfach so viel aufzuräumen ...

Erstens ist das Kopieren und Austauschen nicht immer der richtige Weg, um die Kopierzuweisung zu implementieren. Mit ziemlicher Sicherheit dumb_arrayist dies eine suboptimale Lösung.

Die Verwendung von Kopieren und Tauschen ist dumb_arrayein klassisches Beispiel dafür, wie die teuerste Operation mit den vollsten Funktionen in der unteren Ebene platziert wird. Es ist perfekt für Kunden, die die volle Funktionalität wünschen und bereit sind, die Leistungsstrafe zu zahlen. Sie bekommen genau das, was sie wollen.

Es ist jedoch katastrophal für Kunden, die nicht die volle Funktionalität benötigen und stattdessen die höchste Leistung suchen. Für sie dumb_arrayist es nur eine weitere Software, die sie neu schreiben müssen, weil sie zu langsam ist. Wäre dumb_arrayes anders gestaltet worden, hätte es beide Kunden ohne Kompromisse für beide Kunden zufrieden stellen können.

Der Schlüssel zur Zufriedenheit beider Clients besteht darin, die schnellsten Vorgänge auf der niedrigsten Ebene zu erstellen und anschließend eine API hinzuzufügen, um umfassendere Funktionen zu höheren Kosten zu erhalten. Dh du brauchst die starke Ausnahmegarantie, gut, du bezahlst dafür. Du brauchst es nicht Hier ist eine schnellere Lösung.

Lassen Sie uns konkret werden: Hier ist der schnelle, grundlegende Ausnahmegarantie-Operator für die Zuweisung von Kopien für 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;
}

Erläuterung:

Eines der teureren Dinge, die Sie mit moderner Hardware tun können, ist ein Ausflug auf den Haufen. Alles, was Sie tun können, um einen Ausflug auf den Haufen zu vermeiden, ist Zeit und Mühe, die gut angelegt sind. Clients von dumb_arraymöchten möglicherweise häufig Arrays derselben Größe zuweisen. Und wenn sie es tun, müssen Sie nur ein memcpy(versteckt unter std::copy) tun . Sie möchten kein neues Array derselben Größe zuweisen und dann das alte Array derselben Größe freigeben!

Nun zu Ihren Kunden, die tatsächlich eine starke Ausnahmesicherheit wünschen:

template <class C>
C&
strong_assign(C& lhs, C rhs)
{
    swap(lhs, rhs);
    return lhs;
}

Oder wenn Sie die Verschiebungszuweisung in C ++ 11 nutzen möchten, sollte dies sein:

template <class C>
C&
strong_assign(C& lhs, C rhs)
{
    lhs = std::move(rhs);
    return lhs;
}

Wenn dumb_arraydie Clients Wert auf Geschwindigkeit legen, sollten sie die anrufen operator=. Wenn sie eine starke Ausnahmesicherheit benötigen, können sie generische Algorithmen aufrufen, die für eine Vielzahl von Objekten funktionieren und nur einmal implementiert werden müssen.

Nun zurück zur ursprünglichen Frage (die zu diesem Zeitpunkt einen Typ-O hat):

Class&
Class::operator=(Class&& rhs)
{
    if (this == &rhs)  // is this check needed?
    {
       // ...
    }
    return *this;
}

Dies ist eigentlich eine kontroverse Frage. Einige werden ja sagen, absolut, andere werden nein sagen.

Meine persönliche Meinung ist nein, Sie brauchen diesen Scheck nicht.

Begründung:

Wenn ein Objekt an eine r-Wert-Referenz gebunden wird, ist dies eines von zwei Dingen:

  1. Eine temporäre.
  2. Ein Objekt, an das der Anrufer glauben soll, ist vorübergehend.

Wenn Sie einen Verweis auf ein Objekt haben, das tatsächlich temporär ist, haben Sie per Definition einen eindeutigen Verweis auf dieses Objekt. Es kann unmöglich von irgendwo anders in Ihrem gesamten Programm referenziert werden. Dh this == &temporary ist nicht möglich .

Wenn Ihr Kunde Sie angelogen und Ihnen versprochen hat, dass Sie eine vorübergehende Behandlung erhalten, wenn Sie dies nicht tun, liegt es in der Verantwortung des Kunden, sicherzustellen, dass Sie sich nicht darum kümmern müssen. Wenn Sie wirklich vorsichtig sein möchten, glaube ich, dass dies eine bessere Implementierung wäre:

Class&
Class::operator=(Class&& other)
{
    assert(this != &other);
    // ...
    return *this;
}

Dh Wenn Sie sind eine selbst Referenz übergeben, das ist ein Fehler auf Seiten des Kunden , die behoben werden sollen.

Der Vollständigkeit halber ist hier ein Verschiebungszuweisungsoperator für 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;
}

Im typischen Anwendungsfall der Verschiebungszuweisung handelt *thises sich um ein verschobenes Objekt und delete [] mArray;sollte daher ein No-Op sein. Es ist wichtig, dass Implementierungen das Löschen auf einem Nullptr so schnell wie möglich machen.

Vorbehalt:

Einige werden argumentieren, dass dies swap(x, x)eine gute Idee oder nur ein notwendiges Übel ist. Und dies kann, wenn der Swap zum Standard-Swap wechselt, eine Selbstbewegungszuweisung verursachen.

Ich bin nicht einverstanden , dass swap(x, x)ist immer eine gute Idee. Wenn es in meinem eigenen Code gefunden wird, betrachte ich es als Leistungsfehler und behebe es. swap(x, x)Wenn Sie dies jedoch zulassen möchten, müssen Sie sich darüber im Klaren sein , dass Self-Move-Assignemnet nur für einen Verschiebungswert verwendet wird. Und in unserem dumb_arrayBeispiel ist dies vollkommen harmlos, wenn wir die Behauptung einfach weglassen oder sie auf den Fall beschränken, aus dem wir uns entfernt haben:

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

Wenn Sie zwei verschobene (leere) selbst zuweisen dumb_array, tun Sie nichts Falsches, außer nutzlose Anweisungen in Ihr Programm einzufügen. Dieselbe Beobachtung kann für die überwiegende Mehrheit der Objekte gemacht werden.

<Aktualisieren>

Ich habe über dieses Problem noch etwas nachgedacht und meine Position etwas geändert. Ich bin jetzt der Meinung, dass die Zuweisung tolerant gegenüber der Selbstzuweisung sein sollte, aber dass die Post-Bedingungen für die Zuweisung von Kopien und die Zuweisung von Verschiebungen unterschiedlich sind:

Für die Zuordnung von Kopien:

x = y;

man sollte eine Nachbedingung haben, dass der Wert von ynicht geändert werden sollte. Wenn &x == &ydann diese Nachbedingung übersetzt wird in: Selbstkopiezuweisung sollte keinen Einfluss auf den Wert von haben x.

Für die Zugzuweisung:

x = std::move(y);

man sollte eine Nachbedingung haben, ydie einen gültigen, aber nicht spezifizierten Zustand hat. Wenn &x == &ydann diese Nachbedingung übersetzt wird: xhat einen gültigen, aber nicht spezifizierten Zustand. Das heißt, die Selbstbewegungsaufgabe muss kein No-Op sein. Aber es sollte nicht abstürzen. Diese Nachbedingung steht im Einklang mit der Erlaubnis swap(x, x), nur zu arbeiten:

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
}

Das obige funktioniert, solange x = std::move(x)es nicht abstürzt. Es kann xin jedem gültigen, aber nicht spezifizierten Zustand belassen werden.

Ich sehe drei Möglichkeiten, den Verschiebungszuweisungsoperator zu programmieren dumb_array, um dies zu erreichen:

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

Die obige Umsetzung toleriert Selbstzuweisung, sondern *thisund otherein Null-sized Array nach der Selbst bewegt Zuordnung am Ende wird, unabhängig davon , was der ursprüngliche Wert *thisist. Das ist in Ordnung.

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

Die obige Implementierung toleriert die Selbstzuweisung genauso wie der Kopierzuweisungsoperator, indem sie zu einem No-Op gemacht wird. Das ist auch gut so.

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

Das Obige ist nur in Ordnung, wenn dumb_arrayes keine Ressourcen enthält, die "sofort" zerstört werden sollten. Wenn zum Beispiel die einzige Ressource Speicher ist, ist das oben Genannte in Ordnung. Wenn dumb_arraymöglicherweise Mutex-Sperren oder der geöffnete Status von Dateien vorhanden sein könnten, könnte der Client vernünftigerweise erwarten, dass diese Ressourcen auf der linken Seite der Verschiebungszuweisung sofort freigegeben werden, und daher könnte diese Implementierung problematisch sein.

Die Kosten für das erste sind zwei zusätzliche Geschäfte. Die Kosten für die Sekunde sind Test und Verzweigung. Beide arbeiten. Beide erfüllen alle Anforderungen von Tabelle 22 MoveAssignable-Anforderungen im C ++ 11-Standard. Der dritte funktioniert auch modulo das Nicht-Speicher-Ressourcen-Problem.

Alle drei Implementierungen können je nach Hardware unterschiedliche Kosten verursachen: Wie teuer ist eine Niederlassung? Gibt es viele oder nur sehr wenige Register?

Das Mitnehmen ist, dass die Selbstverschiebungszuweisung im Gegensatz zur Selbstkopierzuweisung nicht den aktuellen Wert beibehalten muss.

</Aktualisieren>

Eine letzte (hoffentlich) Bearbeitung, die von Luc Dantons Kommentar inspiriert wurde:

Wenn Sie eine Klasse auf hoher Ebene schreiben, die den Speicher nicht direkt verwaltet (aber möglicherweise Basen oder Mitglieder hat, die dies tun), ist die beste Implementierung der Verschiebungszuweisung häufig:

Class& operator=(Class&&) = default;

Dadurch werden jede Basis und jedes Mitglied nacheinander zugewiesen und es wird kein this != &otherScheck hinzugefügt. Dies bietet Ihnen die höchste Leistung und grundlegende Ausnahmesicherheit, vorausgesetzt, dass keine Invarianten zwischen Ihren Basen und Mitgliedern beibehalten werden müssen. Zeigen Sie Ihren Kunden, die eine starke Ausnahmesicherheit fordern, diese strong_assign.

Howard Hinnant
quelle
6
Ich weiß nicht, wie ich mich bei dieser Antwort fühlen soll. Es sieht so aus, als ob die Implementierung solcher Klassen (die ihren Speicher sehr explizit verwalten) eine häufige Aufgabe ist. Es ist wahr , dass , wenn Sie tun Schreib eine solche Klasse eine sehr extrem vorsichtig Ausnahme Sicherheitsgarantien sein und den Sweet Spot zu finden , für die Schnittstelle kurze , aber bequem zu sein, aber die Frage scheint für die allgemeine Beratung zu fragen.
Luc Danton
Ja, ich verwende Copy-and-Swap definitiv nie, weil es Zeitverschwendung für Klassen ist, die Ressourcen und Dinge verwalten (warum sollten Sie eine weitere vollständige Kopie aller Ihrer Daten erstellen?). Und danke, das beantwortet meine Frage.
Seth Carnegie
5
Abgestimmt für den Vorschlag, dass die Zuweisung von Bewegungen von sich aus jemals fehlschlagen oder zu einem "nicht spezifizierten" Ergebnis führen sollte. Selbstzuweisung ist buchstäblich der einfachste Fall , um richtig zu machen. Wenn Ihre Klasse abstürzt std::swap(x,x), warum sollte ich dann darauf vertrauen, dass kompliziertere Vorgänge korrekt ausgeführt werden?
Quuxplusone
1
@Quuxplusone: Ich bin gekommen, um Ihnen in Bezug auf den Assert-Fail zuzustimmen, wie im Update zu meiner Antwort vermerkt. Soweit es std::swap(x,x)geht, funktioniert es nur , wenn x = std::move(x)ein nicht spezifiziertes Ergebnis erzielt wird. Versuch es! Du musst mir nicht glauben.
Howard Hinnant
@HowardHinnant guter Punkt, swapfunktioniert so lange wie x = move(x)Blätter xin einem beweglichen Zustand. Und die std::copy/ std::move-Algorithmen sind so definiert, dass sie bereits auf No-Op-Kopien ein undefiniertes Verhalten erzeugen (autsch; der 20-Jährige memmovemacht den trivialen Fall richtig, std::movetut es aber nicht!). Ich denke also, ich habe noch nicht an einen "Slam Dunk" zur Selbstzuweisung gedacht. Aber offensichtlich ist Selbstzuweisung etwas, was im realen Code häufig vorkommt, unabhängig davon, ob der Standard ihn gesegnet hat oder nicht.
Quuxplusone
11

Erstens haben Sie die Signatur des Verschiebungszuweisungsoperators falsch verstanden. Da Bewegungen Ressourcen aus dem Quellobjekt stehlen, muss die Quelle eine constReferenz ohne r-Wert sein.

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

Beachten Sie, dass Sie immer noch über eine (nicht const) l- Wert-Referenz zurückkehren.

Bei beiden Arten der direkten Zuweisung besteht der Standard nicht darin, die Selbstzuweisung zu überprüfen, sondern sicherzustellen, dass eine Selbstzuweisung keinen Absturz verursacht. Im Allgemeinen tut x = xoder y = std::move(y)ruft niemand explizit auf , aber Aliasing, insbesondere durch mehrere Funktionen, kann zu Selbstzuweisungen führen a = boder dazu führen c = std::move(d). Eine explizite Überprüfung der Selbstzuweisung, dh this == &rhs, dass das Fleisch der Funktion übersprungen wird, wenn sie wahr ist, ist eine Möglichkeit, die Sicherheit der Selbstzuweisung zu gewährleisten. Dies ist jedoch eine der schlimmsten Möglichkeiten, da ein (hoffentlich) seltener Fall optimiert wird, während es eine Anti-Optimierung für den häufigeren Fall darstellt (aufgrund von Verzweigungen und möglicherweise Cache-Fehlern).

Wenn (mindestens) einer der Operanden ein direkt temporäres Objekt ist, kann es niemals zu einem Selbstzuweisungsszenario kommen. Einige Leute befürworten die Annahme dieses Falls und optimieren den Code so sehr, dass der Code selbstmörderisch dumm wird, wenn die Annahme falsch ist. Ich sage, dass es unverantwortlich ist, die gleiche Objektprüfung auf Benutzer abzulegen. Wir machen dieses Argument nicht für die Zuweisung von Kopien; Warum die Position für die Bewegungszuweisung umkehren?

Lassen Sie uns ein Beispiel machen, das von einem anderen Befragten geändert wurde:

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

Diese Kopierzuweisung behandelt die Selbstzuweisung ohne explizite Prüfung ordnungsgemäß. Wenn sich die Quell- und Zielgrößen unterscheiden, werden die Freigabe und die Neuzuweisung vor dem Kopieren vorgenommen. Andernfalls wird nur das Kopieren durchgeführt. Die Selbstzuweisung erhält keinen optimierten Pfad, sondern wird in denselben Pfad verschoben, in dem die Quell- und Zielgröße gleich beginnen. Das Kopieren ist technisch nicht erforderlich, wenn die beiden Objekte gleichwertig sind (auch wenn sie dasselbe Objekt sind), aber das ist der Preis, wenn keine Gleichheitsprüfung (wert- oder adressbezogen) durchgeführt wird, da diese Prüfung selbst am meisten Verschwendung wäre der ganzen Zeit. Beachten Sie, dass die Selbstzuweisung von Objekten hier eine Reihe von Selbstzuweisungen auf Elementebene verursacht. Der Elementtyp muss dafür sicher sein.

Wie das Quellbeispiel bietet diese Zuweisung die grundlegende Sicherheitsgarantie für Ausnahmen. Wenn Sie die starke Garantie wünschen, verwenden Sie den Unified-Assignment-Operator aus der ursprünglichen Copy and Swap- Abfrage, der sowohl die Copy- als auch die Move-Zuweisung behandelt. In diesem Beispiel geht es jedoch darum, die Sicherheit um einen Rang zu verringern, um an Geschwindigkeit zu gewinnen. (Übrigens gehen wir davon aus, dass die Werte der einzelnen Elemente unabhängig sind; dass es keine invariante Einschränkung gibt, die einige Werte im Vergleich zu anderen einschränkt.)

Schauen wir uns eine Verschiebungszuweisung für denselben Typ an:

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

Ein austauschbarer Typ, der angepasst werden muss, sollte eine Funktion mit zwei Argumenten haben, die swapim selben Namespace wie der Typ aufgerufen wird . (Durch die Namespace-Einschränkung können nicht qualifizierte Aufrufe zum Arbeiten ausgetauscht werden.) Ein Containertyp sollte auch eine öffentliche swapMember-Funktion hinzufügen , die den Standardcontainern entspricht. Wenn ein Mitglied swapnicht bereitgestellt wird, muss die freie Funktion swapwahrscheinlich als Freund des austauschbaren Typs markiert werden. Wenn Sie die zu verwendenden Moves anpassen swap, müssen Sie Ihren eigenen Swap-Code angeben. Der Standardcode ruft den Verschiebungscode des Typs auf, was zu einer unendlichen gegenseitigen Rekursion für verschiebungsangepasste Typen führen würde.

Wie Destruktoren sollten Swap-Funktionen und Verschiebungsoperationen, wenn möglich, niemals ausgelöst und wahrscheinlich als solche markiert werden (in C ++ 11). Standardbibliothekstypen und -routinen verfügen über Optimierungen für nicht wegwerfbare Bewegungstypen.

Diese erste Version der Umzugszuweisung erfüllt den Grundvertrag. Die Ressourcenmarkierungen der Quelle werden an das Zielobjekt übertragen. Die alten Ressourcen werden nicht verloren gehen, da das Quellobjekt sie jetzt verwaltet. Das Quellobjekt befindet sich in einem verwendbaren Zustand, in dem weitere Operationen, einschließlich Zuweisung und Zerstörung, darauf angewendet werden können.

Beachten Sie, dass diese Verschiebungszuweisung automatisch für die Selbstzuweisung sicher ist, da der swapAnruf erfolgt. Es ist auch stark ausnahmesicher. Das Problem ist die unnötige Ressourcenbindung. Die alten Ressourcen für das Ziel werden konzeptionell nicht mehr benötigt, aber hier sind sie nur noch vorhanden, damit das Quellobjekt gültig bleiben kann. Wenn die geplante Zerstörung des Quellobjekts weit entfernt ist, verschwenden wir Ressourcenraum, oder schlimmer noch, wenn der gesamte Ressourcenraum begrenzt ist und andere Ressourcenanträge gestellt werden, bevor das (neue) Quellobjekt offiziell stirbt.

Dieses Problem hat den umstrittenen aktuellen Guru-Rat bezüglich der Selbstausrichtung während der Zuweisung von Bewegungen verursacht. Die Art und Weise, eine Zugzuweisung zu schreiben, ohne Ressourcen zu belassen, ist wie folgt:

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

Die Quelle wird auf die Standardbedingungen zurückgesetzt, während die alten Zielressourcen zerstört werden. Im Fall der Selbstzuweisung begeht Ihr aktuelles Objekt Selbstmord. Die Hauptmethode besteht darin, den Aktionscode mit einem if(this != &other)Block zu umgeben oder ihn zu verschrauben und den Kunden eine assert(this != &other)erste Zeile zu geben (wenn Sie sich gut fühlen).

Eine Alternative besteht darin, zu untersuchen, wie die Zuweisung von Kopien ohne einheitliche Zuweisung stark ausnahmesicher gemacht werden kann, und sie auf die Zuweisung von Verschiebungen anzuwenden:

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

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

Wann otherund thissind verschieden, otherwird durch den Umzug nach geleert tempund bleibt so. Dann thisverliert seine alten Ressourcen , tempwährend sich die Ressourcen ursprünglich aus Beständen other. Dann werden die alten Ressourcen thisgetötet, wenn dies der tempFall ist.

Wenn Selbstzuweisung stattfindet other, templeert sich thisauch das Entleeren in . Dann erhält das Zielobjekt seine Ressourcen zurück, wenn tempund thistauscht. Der Tod von tempbehauptet ein leeres Objekt, das praktisch ein No-Op sein sollte. Das this/ otherObjekt behält seine Ressourcen.

Die Zugzuweisung sollte niemals geworfen werden, solange Zugbewegung und Tausch ebenfalls möglich sind. Die Kosten, um auch während der Selbstzuweisung sicher zu sein, sind ein paar weitere Anweisungen für Typen auf niedriger Ebene, die durch den Freigabeaufruf überflutet werden sollten.

CTMacUser
quelle
Müssen Sie überprüfen, ob Speicher zugewiesen wurde, bevor Sie deleteIhren zweiten Codeblock aufrufen ?
user3728501
3
Ihr zweites Codebeispiel, der Kopierzuweisungsoperator ohne Selbstzuweisungsprüfung, ist falsch. std::copyverursacht undefiniertes Verhalten, wenn sich die Quell- und Zielbereiche überschneiden (einschließlich des Falls, wenn sie zusammenfallen). Siehe C ++ 14 [alg.copy] / 3.
MM
6

Ich bin im Lager derer, die sichere Operatoren für die Selbstzuweisung wollen, aber keine Selbstzuweisungsprüfungen in den Implementierungen von schreiben möchten operator=. Und in der Tat möchte ich überhaupt nicht implementieren operator=, ich möchte, dass das Standardverhalten "sofort" funktioniert. Die besten Sondermitglieder sind diejenigen, die kostenlos kommen.

Davon abgesehen werden die im Standard enthaltenen MoveAssignable-Anforderungen wie folgt beschrieben (ab 17.6.3.1 Anforderungen an Vorlagenargumente [Utility.arg.requirements], n3290):

Ausdruck Rückgabetyp Rückgabewert Nachbedingung
t = rv T & tt entspricht dem Wert von rv vor der Zuweisung

wobei die Platzhalter wie tfolgt beschrieben werden: " [ist ein] modifizierbarer Wert vom Typ T;" und " rvist ein Wert vom Typ T;". Beachten Sie, dass dies Anforderungen an die Typen sind, die als Argumente für die Vorlagen der Standardbibliothek verwendet werden. Wenn ich jedoch an anderer Stelle im Standard nachschaue, stelle ich fest, dass jede Anforderung an die Verschiebungszuweisung dieser ähnlich ist.

Das heißt, a = std::move(a)das muss "sicher" sein. Wenn Sie einen Identitätstest benötigen (z. B. this != &other), dann machen Sie ihn, sonst können Sie Ihre Objekte nicht einmal hineinlegen std::vector! (Es sei denn , Sie nicht die Mitglieder verwenden / Operationen , die erfordern MoveAssignable, aber macht nichts , dass.) Beachten Sie, dass mit dem vorherigen Beispiel a = std::move(a), dann this == &otherwird in der Tat halten.

Luc Danton
quelle
Können Sie erklären, wie das a = std::move(a)Nicht-Arbeiten dazu führen würde, dass eine Klasse nicht funktioniert std::vector? Beispiel?
Paul J. Lucas
@ PaulJ.Lucas Das Aufrufen std::vector<T>::eraseist nur zulässig, wenn TMoveAssignable aktiviert ist. (Abgesehen von IIRC wurden einige MoveAssignable-Anforderungen stattdessen in C ++ 14 auf MoveInsertable gelockert.)
Luc Danton
OK, Tmuss also MoveAssignable sein, aber warum sollte man erase()jemals davon abhängen, ein Element in sich selbst zu verschieben ?
Paul J. Lucas
@ PaulJ.Lucas Auf diese Frage gibt es keine zufriedenstellende Antwort. Alles läuft darauf hinaus, Verträge nicht zu brechen.
Luc Danton
2

Da Ihre aktuelle operator=Funktion geschrieben ist, können Sie, da Sie das Argument rvalue-reference erstellt haben const, die Zeiger auf keinen Fall "stehlen" und die Werte der eingehenden rvalue-Referenz ändern. Sie können sie einfach nicht ändern konnte nur daraus lesen. Ich würde nur dann ein Problem sehen, wenn Sie anfangen würden, deleteZeiger usw. in Ihrem thisObjekt aufzurufen , wie Sie es bei einer normalen lvaue-Referenzmethode tun würden operator=, aber diese Art besiegt den Punkt der rvalue-Version ... dh es würde Es scheint überflüssig zu sein, die rvalue-Version zu verwenden, um im Grunde die gleichen Operationen auszuführen, die normalerweise einer const-lvalue- operator=Methode überlassen werden.

Wenn Sie nun definiert haben operator=, dass Sie eine Nicht- constWert-Referenz verwenden sollen, konnte ich nur sehen, dass eine Prüfung erforderlich ist, wenn Sie das thisObjekt an eine Funktion übergeben haben, die absichtlich eine Wert-Referenz anstelle einer temporären Referenz zurückgegeben hat.

Angenommen, jemand hat versucht, eine operator+Funktion zu schreiben , und eine Mischung aus r-Wert-Referenzen und l-Wert-Referenzen verwendet, um zu verhindern, dass während einer gestapelten Additionsoperation für den Objekttyp zusätzliche Temporäre erstellt werden:

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
}

Nach meinem Verständnis von rvalue-Referenzen wird davon abgeraten, das oben Gesagte zu tun (dh Sie sollten nur eine temporäre, keine rvalue-Referenz zurückgeben). Wenn dies jedoch noch jemand tun würde, sollten Sie dies überprüfen Stellen Sie sicher, dass die eingehende rWert-Referenz nicht auf dasselbe Objekt wie der thisZeiger verweist .

Jason
quelle
Beachten Sie, dass "a = std :: move (a)" ein trivialer Weg ist, um diese Situation zu haben. Ihre Antwort ist jedoch gültig.
Vaughn Cato
1
Stimme voll und ganz zu, dass dies der einfachste Weg ist, obwohl ich denke, dass die meisten Leute das nicht absichtlich tun werden :-) ... Denken Sie jedoch daran, dass Sie, wenn die r-Wert-Referenz lautet const, nur daraus lesen können, also die einzige Notwendigkeit Eine Überprüfung wäre, wenn Sie sich dazu entschließen operator=(const T&&), dieselbe Neuinitialisierung durchzuführen, die thisSie bei einer typischen operator=(const T&)Methode und nicht bei einer Operation im Swap-Stil durchführen würden (dh Zeiger stehlen usw., anstatt tiefe Kopien zu erstellen).
Jason
1

Meine Antwort lautet immer noch, dass die Zuweisung von Zügen nicht gegen Selbstzuweisung gesichert sein muss, sondern eine andere Erklärung hat. Betrachten Sie std :: unique_ptr. Wenn ich eine implementieren würde, würde ich so etwas tun:

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

Wenn Sie sich Scott Meyers ansehen , der dies erklärt , macht er etwas Ähnliches. (Wenn Sie wandern, warum nicht tauschen - es hat ein zusätzliches Schreiben). Und dies ist nicht sicher für die Selbstzuweisung.

Manchmal ist das unglücklich. Ziehen Sie in Betracht, alle geraden Zahlen aus dem Vektor zu entfernen:

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

Dies ist für ganze Zahlen in Ordnung, aber ich glaube nicht, dass Sie so etwas mit Verschiebungssemantik zum Laufen bringen können.

Abschließend: Die Zuordnung zum Objekt selbst zu verschieben ist nicht in Ordnung und Sie müssen darauf achten.

Kleines Update.

  1. Ich bin mit Howard nicht einverstanden, was eine schlechte Idee ist, aber dennoch - ich denke, dass die Selbstbewegungszuweisung von "ausgezogenen" Objekten funktionieren sollte, weil swap(x, x)sie funktionieren sollte. Algorithmen lieben diese Dinge! Es ist immer schön, wenn ein Eckkoffer gerade funktioniert. (Und ich muss noch einen Fall sehen, in dem es nicht kostenlos ist. Das heißt aber nicht, dass es nicht existiert).
  2. So wird die Zuweisung von unique_ptrs in libc ++ implementiert: unique_ptr& operator=(unique_ptr&& u) noexcept { reset(u.release()); ...} Es ist sicher für die Zuweisung von Selbstverschiebungen.
  3. Kernrichtlinien denken, dass es in Ordnung sein sollte, sich selbst zu verschieben.
Denis Yaroshevskiy
quelle
0

Es gibt eine Situation, an die ich denken kann (dies == rhs). Für diese Aussage: Myclass obj; std :: move (obj) = std :: move (obj)

little_monster
quelle
Myclass obj; std :: move (obj) = std :: move (obj);
little_monster