Stellen wir uns vor, wir haben eine Struktur für 3 Doubles mit einigen Mitgliedsfunktionen:
struct Vector {
double x, y, z;
// ...
Vector &negate() {
x = -x; y = -y; z = -z;
return *this;
}
Vector &normalize() {
double s = 1./sqrt(x*x+y*y+z*z);
x *= s; y *= s; z *= s;
return *this;
}
// ...
};
Dies ist der Einfachheit halber ein wenig erfunden, aber ich bin sicher, Sie stimmen zu, dass es ähnlichen Code gibt. Mit den Methoden können Sie bequem verketten, zum Beispiel:
Vector v = ...;
v.normalize().negate();
Oder auch:
Vector v = Vector{1., 2., 3.}.normalize().negate();
Wenn wir nun die Funktionen begin () und end () bereitstellen, könnten wir unseren Vektor in einer neuen for-Schleife verwenden, beispielsweise um die 3 Koordinaten x, y und z zu durchlaufen (Sie können zweifellos weitere "nützliche" Beispiele erstellen durch Ersetzen von Vector durch zB String):
Vector v = ...;
for (double x : v) { ... }
Wir können sogar:
Vector v = ...;
for (double x : v.normalize().negate()) { ... }
und auch:
for (double x : Vector{1., 2., 3.}) { ... }
Das Folgende (es scheint mir) ist jedoch kaputt:
for (double x : Vector{1., 2., 3.}.normalize()) { ... }
Während es wie eine logische Kombination der beiden vorherigen Verwendungen erscheint, denke ich, dass diese letzte Verwendung eine baumelnde Referenz erzeugt, während die vorherigen beiden völlig in Ordnung sind.
- Ist das richtig und weithin geschätzt?
- Welcher Teil des oben genannten ist der "schlechte" Teil, der vermieden werden sollte?
- Würde die Sprache verbessert, indem die Definition der bereichsbasierten for-Schleife so geändert wird, dass im for-Ausdruck konstruierte Temporäre für die Dauer der Schleife existieren?
Antworten:
Ja, dein Verständnis der Dinge ist richtig.
Der schlechte Teil besteht darin, eine L-Wert-Referenz auf eine von einer Funktion zurückgegebene temporäre Referenz zu nehmen und sie an eine R-Wert-Referenz zu binden. Es ist genauso schlimm wie das:
Die
Vector{1., 2., 3.}
Lebensdauer des Temporären kann nicht verlängert werden, da der Compiler keine Ahnung hat, dass der Rückgabewert von daraufnormalize
verweist.Das wäre sehr widersprüchlich mit der Funktionsweise von C ++.
Würde es bestimmte Fallstricke verhindern, die von Personen verursacht werden, die verkettete Ausdrücke auf Provisorien oder verschiedene Methoden zur verzögerten Bewertung von Ausdrücken verwenden? Ja. Es würde aber auch speziellen Compiler-Code erfordern und verwirrend sein, warum es nicht mit anderen Ausdruckskonstrukten funktioniert .
Eine viel vernünftigere Lösung wäre eine Möglichkeit, den Compiler darüber zu informieren, dass der Rückgabewert einer Funktion immer eine Referenz
this
ist. Wenn der Rückgabewert also an ein temporär erweiterendes Konstrukt gebunden ist, würde er den korrekten temporären Wert erweitern. Das ist jedoch eine Lösung auf Sprachebene.Derzeit (wenn der Compiler dies unterstützt) können Sie es so gestalten, dass
normalize
es nicht vorübergehend aufgerufen werden kann :Dies führt
Vector{1., 2., 3.}.normalize()
zu einem Kompilierungsfehler undv.normalize()
funktioniert einwandfrei. Offensichtlich können Sie solche Dinge nicht richtig machen:Sie können aber auch keine falschen Dinge tun.
Alternativ können Sie, wie in den Kommentaren vorgeschlagen, festlegen, dass die rvalue-Referenzversion einen Wert anstelle einer Referenz zurückgibt:
Wenn
Vector
es sich um einen Typ mit tatsächlich zu verschiebenden Ressourcen handelt, können Sie ihnVector ret = std::move(*this);
stattdessen verwenden. Durch die Optimierung des benannten Rückgabewerts ist dies in Bezug auf die Leistung einigermaßen optimal.quelle
delete
könnten Sie keine alternative Operation bereitstellen, die einen r-Wert zurückgibt:Vector normalize() && { normalize(); return std::move(*this); }
(Ich glaube, dass der Aufrufnormalize
innerhalb der Funktion zur Überlastung des Werts führt, aber jemand sollte dies überprüfen :)&
/&&
Qualifikation von Methoden noch nie gesehen . Ist dies aus C ++ 11 oder ist dies eine (möglicherweise weit verbreitete) proprietäre Compiler-Erweiterung. Gibt interessante Möglichkeiten.Dies ist keine Einschränkung der Sprache, sondern ein Problem mit Ihrem Code. Der Ausdruck
Vector{1., 2., 3.}
erstellt eine temporäre, aber dienormalize
Funktion gibt eine l-Wert-Referenz zurück . Da der Ausdruck ein Wert ist , geht der Compiler davon aus, dass das Objekt aktiv ist. Da es sich jedoch um eine Referenz auf ein temporäres Objekt handelt, stirbt das Objekt, nachdem der vollständige Ausdruck ausgewertet wurde, sodass Sie eine baumelnde Referenz erhalten.Wenn Sie nun Ihr Design so ändern, dass ein neues Objekt nach Wert und nicht nach einem Verweis auf das aktuelle Objekt zurückgegeben wird, gibt es kein Problem und der Code funktioniert wie erwartet.
quelle
const
Referenz in diesem Fall die Lebensdauer des Objekts verlängern?normalize()
als Mutationsfunktion für ein vorhandenes Objekt brechen . Also die Frage. Dass ein Temporär eine "verlängerte Lebensdauer" hat, wenn er für den spezifischen Zweck einer Iteration verwendet wird, und nicht anders, halte ich für eine verwirrende Fehlfunktion.const&
) hat eine verlängerte Lebensdauer.Vector & r = Vector{1.,2.,3.}.normalize();
. Ihr Design weist diese Einschränkung auf, und das bedeutet, dass Sie entweder bereit sind, nach Wert zurückzukehren (was unter vielen Umständen sinnvoll sein kann, insbesondere bei r-Wert-Referenzen und Verschiebungen ), oder dass Sie das Problem an der Stelle von behandeln müssen call: Erstelle eine richtige Variable und verwende sie dann in der for-Schleife. Beachten Sie auch , dass der AusdruckVector v = Vector{1., 2., 3.}.normalize().negate();
erstellt zwei Objekte ...T const& f(T const&);
ist völlig in Ordnung.T const& t = f(T());
ist völlig in Ordnung. Und dann, in einer anderen TU, entdeckt man dasT const& f(T const& t) { return t; }
und weint ... Wenn manoperator+
mit Werten arbeitet, ist es sicherer ; dann kann der Compiler das Kopieren optimieren (Want Speed? Pass by Values), aber das ist ein Bonus. Die einzige Bindung von Provisorien, die ich zulassen würde, ist die Bindung an r-Werte-Referenzen, aber Funktionen sollten dann aus Sicherheitsgründen Werte zurückgeben und sich auf Copy Elision / Move Semantics verlassen.IMHO ist das zweite Beispiel bereits fehlerhaft. Dass die modifizierenden Operatoren zurückkehren,
*this
ist in der von Ihnen erwähnten Weise praktisch: Es ermöglicht die Verkettung von Modifikatoren. Es kann verwendet werden, um das Ergebnis der Änderung einfach weiterzugeben. Dies ist jedoch fehleranfällig, da es leicht übersehen werden kann. Wenn ich so etwas seheIch würde nicht automatisch vermuten, dass sich die Funktionen
v
als Nebeneffekt ändern . Natürlich könnten sie , aber es wäre verwirrend. Wenn ich also so etwas schreiben würde, würde ich sicherstellen, dassv
das konstant bleibt. Für Ihr Beispiel würde ich kostenlose Funktionen hinzufügenund dann schreiben Sie die Schleifen
und
Das ist IMO besser lesbar und sicherer. Natürlich ist eine zusätzliche Kopie erforderlich, aber für Heap-zugewiesene Daten könnte dies wahrscheinlich in einem billigen C ++ 11-Verschiebungsvorgang erfolgen.
quelle