Was ist die Copy-and-Swap-Sprache?

2001

Was ist diese Redewendung und wann sollte sie verwendet werden? Welche Probleme löst es? Ändert sich die Redewendung, wenn C ++ 11 verwendet wird?

Obwohl es an vielen Stellen erwähnt wurde, hatten wir keine singuläre Frage und Antwort "Was ist das?", Also hier ist es. Hier ist eine unvollständige Liste von Orten, an denen es zuvor erwähnt wurde:

GManNickG
quelle
7
gotw.ca/gotw/059.htm von Herb Sutter
DumbCoder
2
Genial, ich habe diese Frage aus meiner Antwort verknüpft , um die Semantik zu verschieben .
Fredoverflow
4
Es ist eine gute Idee, eine vollständige Erklärung für diese Redewendung zu haben. Es ist so üblich, dass jeder davon wissen sollte.
Matthieu M.
16
Warnung: Die Copy / Swap-Sprache wird weitaus häufiger verwendet, als es nützlich ist. Dies beeinträchtigt häufig die Leistung, wenn bei der Zuweisung von Kopien keine starke Sicherheitsgarantie für Ausnahmen erforderlich ist. Und wenn für die Zuweisung von Kopien eine starke Ausnahmesicherheit erforderlich ist, wird diese neben einem viel schnelleren Operator für die Zuweisung von Kopien problemlos durch eine kurze generische Funktion bereitgestellt. Siehe slidehare.net/ripplelabs/howard-hinnant-accu2014 Folien 43 - 53. Zusammenfassung: Kopieren / Tauschen ist ein nützliches Werkzeug in der Toolbox. Aber es wurde übermarktet und in der Folge oft missbraucht.
Howard Hinnant
2
@ HowardHinnant: Ja, +1 dazu. Ich schrieb dies zu einer Zeit, in der fast jede C ++ - Frage lautete: "Hilf meiner Klasse, beim Kopieren abzustürzen", und dies war meine Antwort. Es ist angemessen, wenn Sie nur Kopier- / Verschiebungssemantik oder was auch immer arbeiten möchten, damit Sie zu anderen Dingen übergehen können, aber es ist nicht wirklich optimal. Fühlen Sie sich frei, einen Haftungsausschluss ganz oben auf meine Antwort zu setzen, wenn Sie denken, dass dies helfen wird.
GManNickG

Antworten:

2184

Überblick

Warum brauchen wir die Copy-and-Swap-Sprache?

Jede Klasse, die eine Ressource verwaltet (ein Wrapper wie ein intelligenter Zeiger), muss The Big Three implementieren . Während die Ziele und die Implementierung des Kopierkonstruktors und des Destruktors unkompliziert sind, ist der Kopierzuweisungsoperator wohl der nuancierteste und schwierigste. Wie soll es gemacht werden? Welche Fallstricke müssen vermieden werden?

Das Copy-and-Swap-Idiom ist die Lösung und unterstützt den Zuweisungsoperator elegant dabei, zwei Dinge zu erreichen: Vermeidung von Codeduplizierungen und Bereitstellung einer starken Ausnahmegarantie .

Wie funktioniert es?

Konzeptionell wird die Funktionalität des Kopierkonstruktors verwendet, um eine lokale Kopie der Daten zu erstellen. Anschließend werden die kopierten Daten mit einer swapFunktion verwendet, wobei die alten Daten gegen die neuen Daten ausgetauscht werden. Die temporäre Kopie wird dann zerstört und nimmt die alten Daten mit. Wir erhalten eine Kopie der neuen Daten.

Um das Copy-and-Swap-Idiom verwenden zu können, benötigen wir drei Dinge: einen funktionierenden Copy-Konstruktor, einen funktionierenden Destruktor (beide sind die Basis eines Wrappers und sollten daher sowieso vollständig sein) und eine swapFunktion.

Eine Swap-Funktion ist eine nicht werfende Funktion, die zwei Objekte einer Klasse Mitglied für Mitglied austauscht. Wir könnten versucht sein, zu verwenden, std::swapanstatt unsere eigenen bereitzustellen, aber dies wäre unmöglich; std::swapverwendet den Kopierkonstruktor und den Kopierzuweisungsoperator in seiner Implementierung, und wir würden letztendlich versuchen, den Zuweisungsoperator in Bezug auf sich selbst zu definieren!

(Nicht nur das, sondern auch unqualifizierte Anrufe an swapverwenden unseren benutzerdefinierten Swap-Operator und überspringen die unnötige Konstruktion und Zerstörung unserer Klasse, die std::swapdies mit sich bringen würde.)


Eine ausführliche Erklärung

Das Ziel

Betrachten wir einen konkreten Fall. Wir wollen in einer ansonsten nutzlosen Klasse ein dynamisches Array verwalten. Wir beginnen mit einem funktionierenden Konstruktor, Kopierkonstruktor und Destruktor:

#include <algorithm> // std::copy
#include <cstddef> // std::size_t

class dumb_array
{
public:
    // (default) constructor
    dumb_array(std::size_t size = 0)
        : mSize(size),
          mArray(mSize ? new int[mSize]() : nullptr)
    {
    }

    // copy-constructor
    dumb_array(const dumb_array& other)
        : mSize(other.mSize),
          mArray(mSize ? new int[mSize] : nullptr),
    {
        // note that this is non-throwing, because of the data
        // types being used; more attention to detail with regards
        // to exceptions must be given in a more general case, however
        std::copy(other.mArray, other.mArray + mSize, mArray);
    }

    // destructor
    ~dumb_array()
    {
        delete [] mArray;
    }

private:
    std::size_t mSize;
    int* mArray;
};

Diese Klasse verwaltet das Array fast erfolgreich, muss jedoch operator=ordnungsgemäß funktionieren.

Eine fehlgeschlagene Lösung

So könnte eine naive Implementierung aussehen:

// the hard part
dumb_array& operator=(const dumb_array& other)
{
    if (this != &other) // (1)
    {
        // get rid of the old data...
        delete [] mArray; // (2)
        mArray = nullptr; // (2) *(see footnote for rationale)

        // ...and put in the new
        mSize = other.mSize; // (3)
        mArray = mSize ? new int[mSize] : nullptr; // (3)
        std::copy(other.mArray, other.mArray + mSize, mArray); // (3)
    }

    return *this;
}

Und wir sagen, wir sind fertig; Dies verwaltet jetzt ein Array ohne Lecks. Es gibt jedoch drei Probleme, die im Code nacheinander als gekennzeichnet sind (n).

  1. Der erste ist der Selbstzuordnungstest. Diese Überprüfung dient zwei Zwecken: Sie verhindert auf einfache Weise, dass bei der Selbstzuweisung unnötiger Code ausgeführt wird, und schützt uns vor subtilen Fehlern (z. B. Löschen des Arrays, nur um zu versuchen, es zu kopieren). In allen anderen Fällen dient es lediglich dazu, das Programm zu verlangsamen und als Rauschen im Code zu wirken. Selbstzuweisung tritt selten auf, daher ist diese Prüfung meistens eine Verschwendung. Es wäre besser, wenn der Bediener ohne sie richtig arbeiten könnte.

  2. Das zweite ist, dass es nur eine grundlegende Ausnahmegarantie bietet. Wenn dies new int[mSize]fehlschlägt, wurde *thises geändert. (Die Größe ist nämlich falsch und die Daten sind weg!) Für eine starke Ausnahmegarantie müsste es sich um Folgendes handeln:

    dumb_array& operator=(const dumb_array& other)
    {
        if (this != &other) // (1)
        {
            // get the new data ready before we replace the old
            std::size_t newSize = other.mSize;
            int* newArray = newSize ? new int[newSize]() : nullptr; // (3)
            std::copy(other.mArray, other.mArray + newSize, newArray); // (3)
    
            // replace the old data (all are non-throwing)
            delete [] mArray;
            mSize = newSize;
            mArray = newArray;
        }
    
        return *this;
    }
    
  3. Der Code wurde erweitert! Was uns zum dritten Problem führt: Codeduplizierung. Unser Zuweisungsoperator dupliziert effektiv den gesamten Code, den wir bereits an anderer Stelle geschrieben haben, und das ist eine schreckliche Sache.

In unserem Fall besteht der Kern nur aus zwei Zeilen (der Zuordnung und der Kopie), aber bei komplexeren Ressourcen kann dieses Aufblähen des Codes ein ziemlicher Aufwand sein. Wir sollten uns bemühen, uns niemals zu wiederholen.

(Man könnte sich fragen: Wenn so viel Code benötigt wird, um eine Ressource korrekt zu verwalten, was ist, wenn meine Klasse mehr als eine verwaltet? Dies scheint zwar ein berechtigtes Problem zu sein, erfordert jedoch nicht triviale try/ catchKlauseln, ist dies jedoch nicht -ausgabe. Das liegt daran, dass eine Klasse nur eine Ressource verwalten sollte !)

Eine erfolgreiche Lösung

Wie bereits erwähnt, behebt das Copy-and-Swap-Idiom alle diese Probleme. Aber im Moment haben wir alle Anforderungen außer einer: eine swapFunktion. Während die Dreierregel erfolgreich die Existenz unseres Kopierkonstruktors, Zuweisungsoperators und Destruktors beinhaltet, sollte sie eigentlich "Die großen Dreieinhalb" heißen: Jedes Mal, wenn Ihre Klasse eine Ressource verwaltet, ist es auch sinnvoll, eine swapFunktion bereitzustellen .

Wir müssen unserer Klasse Swap-Funktionen hinzufügen, und das tun wir wie folgt: †:

class dumb_array
{
public:
    // ...

    friend void swap(dumb_array& first, dumb_array& second) // nothrow
    {
        // enable ADL (not necessary in our case, but good practice)
        using std::swap;

        // by swapping the members of two objects,
        // the two objects are effectively swapped
        swap(first.mSize, second.mSize);
        swap(first.mArray, second.mArray);
    }

    // ...
};

( Hier ist die Erklärung warum public friend swap.) Jetzt können wir nicht nur unsere tauschen dumb_array, sondern Swaps im Allgemeinen können effizienter sein; Es werden lediglich Zeiger und Größen ausgetauscht, anstatt ganze Arrays zuzuweisen und zu kopieren. Abgesehen von diesem Bonus an Funktionalität und Effizienz sind wir jetzt bereit, die Copy-and-Swap-Sprache zu implementieren.

Unser Auftragsoperator ist ohne weiteres:

dumb_array& operator=(dumb_array other) // (1)
{
    swap(*this, other); // (2)

    return *this;
}

Und das ist es! Mit einem Schlag werden alle drei Probleme auf einmal elegant angegangen.

Warum funktioniert es?

Wir bemerken zuerst eine wichtige Wahl: Das Parameterargument wird als Wert genommen . Man könnte zwar genauso gut Folgendes tun (und tatsächlich tun es viele naive Implementierungen der Redewendung):

dumb_array& operator=(const dumb_array& other)
{
    dumb_array temp(other);
    swap(*this, temp);

    return *this;
}

Wir verlieren eine wichtige Optimierungsmöglichkeit . Darüber hinaus ist diese Auswahl in C ++ 11 von entscheidender Bedeutung, das später erläutert wird. (Im Allgemeinen lautet eine bemerkenswert nützliche Richtlinie wie folgt: Wenn Sie eine Kopie von etwas in einer Funktion erstellen möchten, lassen Sie den Compiler dies in der Parameterliste tun. ‡)

In beiden Fällen ist diese Methode zum Abrufen unserer Ressource der Schlüssel zur Vermeidung von Codeduplizierungen: Wir können den Code aus dem Kopierkonstruktor verwenden, um die Kopie zu erstellen, und müssen kein bisschen davon wiederholen. Nachdem die Kopie erstellt wurde, können wir sie austauschen.

Beachten Sie, dass beim Aufrufen der Funktion alle neuen Daten bereits zugewiesen, kopiert und zur Verwendung bereit sind. Dies gibt uns eine starke kostenlose Ausnahmegarantie: Wir werden die Funktion nicht einmal aufrufen, wenn die Erstellung der Kopie fehlschlägt, und es ist daher nicht möglich, den Status von zu ändern *this. (Was wir zuvor für eine starke Ausnahmegarantie manuell gemacht haben, macht der Compiler jetzt für uns; wie nett.)

Zu diesem Zeitpunkt sind wir frei zu Hause, weil wir swapnicht werfen. Wir tauschen unsere aktuellen Daten gegen die kopierten Daten aus, um unseren Status sicher zu ändern, und die alten Daten werden temporär gespeichert. Die alten Daten werden dann freigegeben, wenn die Funktion zurückkehrt. (Wobei der Gültigkeitsbereich des Parameters endet und sein Destruktor aufgerufen wird.)

Da die Redewendung keinen Code wiederholt, können wir keine Fehler im Operator einführen. Beachten Sie, dass dies bedeutet, dass wir keine Selbstzuweisungsprüfung mehr benötigen, um eine einheitliche Implementierung von zu ermöglichen operator=. (Außerdem haben wir keine Leistungseinbußen mehr bei Nicht-Selbstzuweisungen.)

Und das ist die Copy-and-Swap-Sprache.

Was ist mit C ++ 11?

Die nächste Version von C ++, C ++ 11, enthält eine sehr wichtige Änderung bei der Verwaltung von Ressourcen: Die Dreierregel ist jetzt die Viererregel (anderthalb). Warum? Weil wir nicht nur in der Lage sein müssen, unsere Ressource zu kopieren und zu konstruieren, sondern sie auch verschieben und konstruieren müssen .

Zum Glück ist das ganz einfach:

class dumb_array
{
public:
    // ...

    // move constructor
    dumb_array(dumb_array&& other) noexcept ††
        : dumb_array() // initialize via default constructor, C++11 only
    {
        swap(*this, other);
    }

    // ...
};

Was ist denn hier los? Erinnern Sie sich an das Ziel der Bewegungskonstruktion: die Ressourcen einer anderen Instanz der Klasse zu entnehmen und sie in einem Zustand zu belassen, der garantiert zuweisbar und zerstörbar ist.

Was wir also getan haben, ist einfach: Initialisieren Sie über den Standardkonstruktor (eine C ++ 11-Funktion) und tauschen Sie dann mit aus other. Wir wissen, dass eine standardmäßig erstellte Instanz unserer Klasse sicher zugewiesen und zerstört werden kann, sodass wir wissen, dass wir othernach dem Austausch dasselbe tun können.

(Beachten Sie, dass einige Compiler die Konstruktordelegierung nicht unterstützen. In diesem Fall müssen wir die Klasse standardmäßig manuell erstellen. Dies ist eine unglückliche, aber glücklicherweise triviale Aufgabe.)

Warum funktioniert das?

Das ist die einzige Änderung, die wir an unserer Klasse vornehmen müssen. Warum funktioniert das? Denken Sie an die immer wichtige Entscheidung, den Parameter zu einem Wert und nicht zu einer Referenz zu machen:

dumb_array& operator=(dumb_array other); // (1)

Wenn otherjetzt mit einem r-Wert initialisiert wird, wird er verschiebungskonstruiert . Perfekt. Auf die gleiche Weise, wie wir in C ++ 03 unsere Kopierkonstruktorfunktionalität wiederverwenden können, indem wir das Argument als Wert verwenden, wählt C ++ 11 bei Bedarf auch automatisch den Verschiebungskonstruktor aus. (Und natürlich kann, wie in dem zuvor verlinkten Artikel erwähnt, das Kopieren / Verschieben des Werts einfach ganz weggelassen werden.)

Und so schließt die Copy-and-Swap-Sprache.


Fußnoten

* Warum setzen wir mArrayauf null? Denn wenn ein weiterer Code im Operator ausgelöst wird, wird dumb_arraymöglicherweise der Destruktor von aufgerufen. und wenn dies geschieht, ohne es auf null zu setzen, versuchen wir, bereits gelöschten Speicher zu löschen! Wir vermeiden dies, indem wir es auf null setzen, da das Löschen von null keine Operation ist.

† Es gibt andere Behauptungen, dass wir uns auf std::swapunseren Typ spezialisieren, eine Klasse swapneben einer freien Funktion bereitstellen swapsollten usw. Dies ist jedoch alles unnötig: Jede ordnungsgemäße Verwendung swaperfolgt durch einen unqualifizierten Anruf, und unsere Funktion wird es sein gefunden durch ADL . Eine Funktion reicht aus.

‡ Der Grund ist einfach: Sobald Sie die Ressource für sich haben, können Sie sie austauschen und / oder verschieben (C ++ 11), wo immer sie sein muss. Durch Erstellen der Kopie in der Parameterliste maximieren Sie die Optimierung.

†† Der Verschiebungskonstruktor sollte im Allgemeinen sein noexcept, andernfalls std::vectorwird der Kopierkonstruktor von Code (z. B. Größenänderungslogik) verwendet, auch wenn eine Verschiebung sinnvoll wäre. Markieren Sie es natürlich nur dann als nicht, wenn der darin enthaltene Code keine Ausnahmen auslöst.

GManNickG
quelle
17
@GMan: Ich würde argumentieren, dass eine Klasse, die mehrere Ressourcen gleichzeitig verwaltet, zum Scheitern verurteilt ist (Ausnahmesicherheit wird zum Albtraum), und ich würde dringend empfehlen, dass entweder eine Klasse EINE Ressource verwaltet oder über Geschäftsfunktionen verfügt und Manager verwendet.
Matthieu M.
22
Ich verstehe nicht, warum die Swap-Methode hier als Freund deklariert ist.
Szx
9
@asd: Damit es über ADL gefunden werden kann.
GManNickG
8
@neuviemeporte: Mit der Klammer werden die Arrays-Elemente standardmäßig initialisiert. Ohne sind sie nicht initialisiert. Da wir im Kopierkonstruktor die Werte sowieso überschreiben, können wir die Initialisierung überspringen.
GManNickG
10
@neuviemeporte: Sie müssen swapwährend der ADL gefunden werden, wenn Sie möchten, dass es in den meisten generischen Codes funktioniert, auf die Sie stoßen, wie in boost::swapanderen Swap-Instanzen. Swap ist ein heikles Problem in C ++, und im Allgemeinen sind wir uns alle einig, dass ein einzelner Zugriffspunkt (aus Gründen der Konsistenz) am besten ist. Der einzige Weg, dies im Allgemeinen zu tun, ist eine kostenlose Funktion ( intkann kein Swap-Mitglied haben, zum Beispiel). Siehe meine Frage für einige Hintergrundinformationen.
GManNickG
274

Die Zuweisung besteht im Kern aus zwei Schritten: Abreißen des alten Zustands des Objekts und Erstellen seines neuen Zustands als Kopie des Zustands eines anderen Objekts.

Im Grunde ist es das, was der Destruktor und der Kopierkonstruktor tun. Die erste Idee wäre also, die Arbeit an sie zu delegieren. Da die Zerstörung jedoch nicht scheitern darf, während die Konstruktion dies könnte, möchten wir es tatsächlich umgekehrt machen : Führen Sie zuerst den konstruktiven Teil aus und, falls dies erfolgreich war, dann den destruktiven Teil . Das Copy-and-Swap-Idiom ist eine Möglichkeit, genau das zu tun: Es ruft zuerst den Kopierkonstruktor einer Klasse auf, um ein temporäres Objekt zu erstellen, tauscht dann seine Daten mit den temporären aus und lässt dann den Destruktor des temporären Objekts den alten Zustand zerstören.
Schon seitswap()soll niemals scheitern, der einzige Teil, der scheitern könnte, ist die Kopierkonstruktion. Dies wird zuerst ausgeführt, und wenn dies fehlschlägt, wird im Zielobjekt nichts geändert.

In seiner verfeinerten Form wird Copy-and-Swap implementiert, indem die Kopie durch Initialisieren des (Nichtreferenz-) Parameters des Zuweisungsoperators ausgeführt wird:

T& operator=(T tmp)
{
    this->swap(tmp);
    return *this;
}
sbi
quelle
1
Ich denke, dass die Erwähnung des Zuhälters genauso wichtig ist wie die Erwähnung der Kopie, des Austauschs und der Zerstörung. Der Tausch ist nicht magisch ausnahmesicher. Es ist ausnahmesicher, da das Austauschen von Zeigern ausnahmesicher ist. Sie müssen nicht haben einen Pimpl zu verwenden, aber wenn Sie nicht dann tun müssen Sie sicherstellen, dass jeder Swap eines Mitglieds exception-sicher ist. Das kann ein Albtraum sein, wenn sich diese Mitglieder ändern können, und es ist trivial, wenn sie sich hinter einem Pickel verstecken. Und dann kommen die Kosten für den Pickel. Dies führt uns zu dem Schluss, dass Ausnahmesicherheit häufig Leistungskosten verursacht.
Wilhelmtell
7
std::swap(this_string, that)bietet keine No-Throw-Garantie. Es bietet starke Ausnahmesicherheit, aber keine No-Throw-Garantie.
Wilhelmtell
11
@wilhelmtell: In C ++ 03 werden keine Ausnahmen erwähnt, die möglicherweise von std::string::swap(die von aufgerufen werden std::swap) ausgelöst werden . In C ++ 0x std::string::swapist noexceptund darf keine Ausnahme ausgelöst werden .
James McNellis
2
@sbi @JamesMcNellis ok, aber der Punkt bleibt bestehen: Wenn Sie Mitglieder vom Klassentyp haben, müssen Sie sicherstellen, dass das Austauschen kein Wurf ist. Wenn Sie ein einzelnes Mitglied haben, das ein Zeiger ist, ist das trivial. Sonst ist es nicht.
Wilhelmtell
2
@ Wilhelmtell: Ich dachte, das wäre der Punkt des Austauschs: Es wirft nie und es ist immer O (1) (ja, ich weiß, std::array...)
sbi
44

Es gibt bereits einige gute Antworten. Ich werde mich hauptsächlich auf das konzentrieren, was mir meiner Meinung nach fehlt - eine Erklärung der "Nachteile" mit der Copy-and-Swap-Sprache ....

Was ist die Copy-and-Swap-Sprache?

Eine Möglichkeit, den Zuweisungsoperator in Form einer Swap-Funktion zu implementieren:

X& operator=(X rhs)
{
    swap(rhs);
    return *this;
}

Die Grundidee ist:

  • Der fehleranfälligste Teil beim Zuweisen zu einem Objekt besteht darin, sicherzustellen, dass alle Ressourcen, die der neue Status benötigt, erfasst werden (z. B. Speicher, Deskriptoren).

  • Diese Erfassung kann versucht werden, bevor der aktuelle Status des Objekts (dh *this) geändert wird, wenn eine Kopie des neuen Werts erstellt wird, weshalb der Wert eher als Wert (dh als Kopie) als als Referenzrhs akzeptiert wird

  • den Zustand der lokalen Kopie Swapping rhsund *thisist in der Regel relativ einfach zu tun , ohne potenzielle Fehler / Ausnahmen, da die lokale Kopie danach fit (muss nur Zustand für die destructor Lauf benötigt keine besonderen Zustand, so wie für ein Objekt wird verschoben von in> = C ++ 11)

Wann sollte es verwendet werden? (Welche Probleme hat es lösen [/ erstellen] ?)

  • Wenn Sie möchten, dass das zugewiesene Objekt von einer Zuweisung, die eine Ausnahme auslöst, nicht betroffen ist, vorausgesetzt, Sie haben oder können eine swapGarantie mit starker Ausnahme schreiben , und im Idealfall eine, die nicht fehlschlagen kann / throw.. †

  • Wenn Sie eine saubere, leicht verständliche und robuste Methode zum Definieren des Zuweisungsoperators in Bezug auf (einfachere) Kopierkonstruktor- swapund Destruktorfunktionen wünschen .

    • Die Selbstzuweisung als Copy-and-Swap vermeidet häufig übersehene Randfälle. ‡

  • Wenn Leistungseinbußen oder eine vorübergehend höhere Ressourcennutzung, die durch ein zusätzliches temporäres Objekt während der Zuweisung verursacht wird, für Ihre Anwendung nicht wichtig sind. ⁂

swapWerfen: Es ist im Allgemeinen möglich, Datenelemente, die die Objekte nach Zeigern verfolgen, zuverlässig auszutauschen, aber Nicht-Zeiger-Datenelemente, die keinen auswurffreien Austausch haben oder für die das Austauschen als X tmp = lhs; lhs = rhs; rhs = tmp;Kopierkonstruktion oder -zuweisung implementiert werden muss kann werfen, haben immer noch das Potenzial zu scheitern, einige Datenmitglieder ausgetauscht zu lassen und andere nicht. Dieses Potenzial gilt sogar für C ++ 03 std::string, da James eine andere Antwort kommentiert:

@wilhelmtell: In C ++ 03 werden keine Ausnahmen erwähnt, die möglicherweise von std :: string :: swap (das von std :: swap aufgerufen wird) ausgelöst werden. In C ++ 0x ist std :: string :: swap keine Ausnahme und darf keine Ausnahmen auslösen. - James McNellis 22. Dezember 10 um 15:24 Uhr


‡ Die Implementierung eines Zuweisungsoperators, die beim Zuweisen von einem bestimmten Objekt aus vernünftig erscheint, kann bei der Selbstzuweisung leicht fehlschlagen. Während es unvorstellbar erscheint, dass Client-Code sogar versucht, sich selbst zuzuweisen, kann dies bei Algo-Operationen an Containern relativ leicht vorkommen, wobei x = f(x);Code f(möglicherweise nur für einige #ifdefZweige) ein Makro-Ala #define f(x) xoder eine Funktion ist, die einen Verweis auf xoder sogar zurückgibt (wahrscheinlich ineffizient, aber prägnant) Code wie x = c1 ? x * 2 : c2 ? x / 2 : x;). Zum Beispiel:

struct X
{
    T* p_;
    size_t size_;
    X& operator=(const X& rhs)
    {
        delete[] p_;  // OUCH!
        p_ = new T[size_ = rhs.size_];
        std::copy(p_, rhs.p_, rhs.p_ + rhs.size_);
    }
    ...
};

Auf Selbstzuordnung, der obige Code löschen ist x.p_;, Punkte p_in einer neu Haufen Region zugewiesen, dann die zu lesen versucht uninitialised darin Daten (undefiniertes Verhalten), wenn das nicht etwas zu seltsam tut, copyversucht , eine Selbstzuordnung zu jedem nur- zerstörtes 'T'!


⁂ Das Copy-and-Swap-Idiom kann aufgrund der Verwendung eines zusätzlichen temporären Systems zu Ineffizienzen oder Einschränkungen führen (wenn der Parameter des Bedieners kopierkonstruiert ist):

struct Client
{
    IP_Address ip_address_;
    int socket_;
    X(const X& rhs)
      : ip_address_(rhs.ip_address_), socket_(connect(rhs.ip_address_))
    { }
};

Hier Client::operator=könnte eine Handschrift prüfen, ob sie *thisbereits mit demselben Server verbunden ist wie rhs(möglicherweise wird ein "Reset" -Code gesendet, falls dies nützlich ist), während der Copy-and-Swap-Ansatz den Copy-Konstruktor aufruft, der wahrscheinlich zum Öffnen geschrieben wird eine eindeutige Socket-Verbindung schließen Sie dann die ursprüngliche. Dies könnte nicht nur eine Remote-Netzwerkinteraktion anstelle einer einfachen in Bearbeitung befindlichen Variablenkopie bedeuten, sondern auch die Client- oder Serverbeschränkungen für Socket-Ressourcen oder -Verbindungen verletzen. (Natürlich hat diese Klasse eine ziemlich schreckliche Oberfläche, aber das ist eine andere Sache ;-P).

Tony Delroy
quelle
4
Eine Socket-Verbindung war jedoch nur ein Beispiel - das gleiche Prinzip gilt für jede möglicherweise teure Initialisierung, z. B. Hardware-Prüfung / Initialisierung / Kalibrierung, Generieren eines Pools von Threads oder Zufallszahlen, bestimmte Kryptografieaufgaben, Caches, Dateisystem-Scans, Datenbank Verbindungen etc ..
Tony Delroy
Es gibt noch einen (massiven) Betrug. Technisch gesehen hat das Objekt nach den aktuellen technischen Daten keinen Verschiebungszuweisungsoperator! Wenn die neue Klasse später als Mitglied einer Klasse verwendet wird, wird move-ctor nicht automatisch generiert! Quelle: youtu.be/mYrbivnruYw?t=43m14s
user362515
3
Das Hauptproblem mit dem Kopierzuweisungsoperator von Clientist, dass die Zuweisung nicht verboten ist.
sbi
Im Client-Beispiel sollte die Klasse nicht kopierbar gemacht werden.
John Z. Li
25

Diese Antwort ist eher eine Ergänzung und eine geringfügige Änderung der obigen Antworten.

In einigen Versionen von Visual Studio (und möglicherweise anderen Compilern) gibt es einen Fehler, der wirklich ärgerlich und nicht sinnvoll ist. Wenn Sie also Ihre swapFunktion wie folgt deklarieren / definieren :

friend void swap(A& first, A& second) {

    std::swap(first.size, second.size);
    std::swap(first.arr, second.arr);

}

... der Compiler schreit Sie an, wenn Sie die swapFunktion aufrufen :

Geben Sie hier die Bildbeschreibung ein

Dies hat etwas damit zu tun, dass eine friendFunktion aufgerufen und ein thisObjekt als Parameter übergeben wird.


Eine Möglichkeit, dies zu friendumgehen, besteht darin, kein Schlüsselwort zu verwenden und die swapFunktion neu zu definieren :

void swap(A& other) {

    std::swap(size, other.size);
    std::swap(arr, other.arr);

}

Dieses Mal können Sie einfach aufrufen swapund übergeben other, was den Compiler glücklich macht:

Geben Sie hier die Bildbeschreibung ein


Immerhin müssen Sie nicht brauchen eine verwenden friendFunktion Swap - 2 - Objekte. Genauso sinnvoll ist es, swapeine Member-Funktion zu othererstellen , die ein Objekt als Parameter hat.

Sie haben bereits Zugriff auf das thisObjekt, daher ist die Übergabe als Parameter technisch überflüssig.

Oleksiy
quelle
1
@GManNickG dropbox.com/s/o1mitwcpxmawcot/example.cpp dropbox.com/s/jrjrn5dh1zez5vy/Untitled.jpg . Dies ist eine vereinfachte Version. Ein Fehler scheint jedes Mal aufzutreten, wenn eine friendFunktion mit dem *thisParameter
Oleksiy
1
@ GManNickG wie gesagt, es ist ein Fehler und könnte für andere Leute gut funktionieren. Ich wollte nur einigen Menschen helfen, die vielleicht das gleiche Problem haben wie ich. Ich habe dies sowohl mit Visual Studio 2012 Express als auch mit 2013 Preview versucht und das einzige, was es verschwinden ließ, war meine Änderung
Oleksiy
8
@GManNickG es würde nicht in einen Kommentar mit allen Bildern und Codebeispielen passen. Und es ist in Ordnung, wenn die Leute abstimmen. Ich bin sicher, es gibt jemanden da draußen, der den gleichen Fehler bekommt. Die Informationen in diesem Beitrag könnten genau das sein, was sie brauchen.
Oleksiy
14
Beachten Sie, dass dies nur ein Fehler bei der Hervorhebung des IDE-Codes (IntelliSense) ist. Die Kompilierung erfolgt ohne Warnungen / Fehler.
Amro
3
Bitte melden Sie den VS-Fehler hier, wenn Sie dies noch nicht getan haben (und wenn er nicht behoben wurde). Connect.microsoft.com/VisualStudio
Matt
15

Ich möchte ein Wort der Warnung hinzufügen, wenn Sie sich mit Allokator-fähigen Containern im C ++ 11-Stil befassen. Swapping und Assignment haben eine subtil unterschiedliche Semantik.

Betrachten wir der Vollständigkeit halber einen Container std::vector<T, A>, bei dem Aes sich um einen Stateful Allocator-Typ handelt, und vergleichen Sie die folgenden Funktionen:

void fs(std::vector<T, A> & a, std::vector<T, A> & b)
{ 
    a.swap(b);
    b.clear(); // not important what you do with b
}

void fm(std::vector<T, A> & a, std::vector<T, A> & b)
{
    a = std::move(b);
}

Der Zweck beider Funktionen fsund fmist es, aden Zustand zu geben , bder ursprünglich hatte. Es gibt jedoch eine versteckte Frage: Was passiert, wenn a.get_allocator() != b.get_allocator()? Die Antwort lautet: Es kommt darauf an. Lass uns schreiben AT = std::allocator_traits<A>.

  • Wenn dies der Fall AT::propagate_on_container_move_assignmentist std::true_type, wird fmder Allokator von amit dem Wert von neu zugewiesen b.get_allocator(), andernfalls wird dies nicht der Fall sein, und ader ursprüngliche Allokator wird weiterhin verwendet. In diesem Fall müssen die Datenelemente einzeln ausgetauscht werden, da die Speicherung von aund bnicht kompatibel ist.

  • Wenn dies der Fall AT::propagate_on_container_swapist std::true_type, werden fssowohl Daten als auch Allokatoren in der erwarteten Weise ausgetauscht.

  • Wenn AT::propagate_on_container_swapja std::false_type, dann brauchen wir eine dynamische Prüfung.

    • Wenn ja a.get_allocator() == b.get_allocator(), verwenden die beiden Container kompatiblen Speicher, und der Austausch erfolgt auf die übliche Weise.
    • Wenn jedoch a.get_allocator() != b.get_allocator(), hat das Programm ein undefiniertes Verhalten (vgl. [Container.requirements.general / 8].

Das Ergebnis ist, dass das Austauschen in C ++ 11 zu einer nicht trivialen Operation geworden ist, sobald Ihr Container Stateful Allocators unterstützt. Dies ist ein etwas "fortgeschrittener Anwendungsfall", aber nicht ganz unwahrscheinlich, da Verschiebungsoptimierungen normalerweise erst dann interessant werden, wenn Ihre Klasse eine Ressource verwaltet und der Speicher eine der beliebtesten Ressourcen ist.

Kerrek SB
quelle