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_array
ist dies eine suboptimale Lösung.
Die Verwendung von Kopieren und Tauschen ist dumb_array
ein 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_array
ist es nur eine weitere Software, die sie neu schreiben müssen, weil sie zu langsam ist. Wäre dumb_array
es 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_array
mö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_array
die 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:
- Eine temporäre.
- 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 *this
es 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_array
Beispiel 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 y
nicht geändert werden sollte. Wenn &x == &y
dann 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, y
die einen gültigen, aber nicht spezifizierten Zustand hat. Wenn &x == &y
dann diese Nachbedingung übersetzt wird: x
hat 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 x
in 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 *this
und other
ein Null-sized Array nach der Selbst bewegt Zuordnung am Ende wird, unabhängig davon , was der ursprüngliche Wert *this
ist. 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_array
es 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_array
mö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 != &other
Scheck 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
.