Ist dies eine bekannte Falle von C ++ 11 für Schleifen?

89

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?
ndkrempel
quelle
Aus irgendeinem Grund erinnere ich mich an eine sehr ähnliche Frage, die zuvor gestellt wurde, vergaß jedoch, wie sie genannt wurde.
Pubby
Ich halte das für einen Sprachfehler. Die Lebensdauer von Provisorien erstreckt sich nicht auf den gesamten Körper der for-Schleife, sondern nur für die Einrichtung der for-Schleife. Es ist nicht nur die Bereichssyntax, die darunter leidet, sondern auch die klassische Syntax. Meiner Meinung nach sollte sich die Lebensdauer von Provisorien in der init-Anweisung über die gesamte Lebensdauer der Schleife erstrecken.
edA-qa mort-ora-y
1
@ edA-qamort-ora-y: Ich stimme eher zu, dass hier ein kleiner Sprachfehler lauert, aber ich denke, es ist speziell die Tatsache, dass die Verlängerung der Lebensdauer implizit erfolgt, wenn Sie eine temporäre Datei direkt an eine Referenz binden, aber in keiner andere Situation - dies scheint eine halbherzige Lösung für das zugrunde liegende Problem der vorübergehenden Lebensdauer zu sein, obwohl das nicht heißt, dass es offensichtlich ist, was eine bessere Lösung wäre. Vielleicht eine explizite Syntax für die Verlängerung der Lebensdauer beim Erstellen des temporären Blocks, die bis zum Ende des aktuellen Blocks gültig ist - was denken Sie?
Ndkrempel
@ edA-qamort-ora-y: ... dies entspricht dem Binden des Temporären an eine Referenz, hat jedoch den Vorteil, dass es für den Leser expliziter ist, dass eine "Verlängerung der Lebensdauer" inline (in einem Ausdruck) auftritt (anstatt eine separate Erklärung zu verlangen), und Sie müssen die temporäre nicht benennen.
Ndkrempel
1
mögliches Duplikat des temporären Objekts in
bereichsbasiert

Antworten:

64

Ist das richtig und weithin geschätzt?

Ja, dein Verständnis der Dinge ist richtig.

Welcher Teil des oben genannten ist der "schlechte" Teil, der vermieden werden sollte?

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:

auto &&t = Vector{1., 2., 3.}.normalize();

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 darauf normalizeverweist.

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?

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 thisist. 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 :

struct Vector {
  double x, y, z;
  // ...
  Vector &normalize() & {
     double s = 1./sqrt(x*x+y*y+z*z);
     x *= s; y *= s; z *= s;
     return *this;
  }
  Vector &normalize() && = delete;
};

Dies führt Vector{1., 2., 3.}.normalize()zu einem Kompilierungsfehler und v.normalize()funktioniert einwandfrei. Offensichtlich können Sie solche Dinge nicht richtig machen:

Vector t = Vector{1., 2., 3.}.normalize();

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:

struct Vector {
  double x, y, z;
  // ...
  Vector &normalize() & {
     double s = 1./sqrt(x*x+y*y+z*z);
     x *= s; y *= s; z *= s;
     return *this;
  }
  Vector normalize() && {
     Vector ret = *this;
     ret.normalize();
     return ret;
  }
};

Wenn Vectores sich um einen Typ mit tatsächlich zu verschiebenden Ressourcen handelt, können Sie ihn Vector ret = std::move(*this);stattdessen verwenden. Durch die Optimierung des benannten Rückgabewerts ist dies in Bezug auf die Leistung einigermaßen optimal.

Nicol Bolas
quelle
1
Die Sache, die dies eher zu einem "Gotcha" machen könnte, ist, dass die neue for-Schleife die Tatsache syntaktisch verbirgt, dass die Referenzbindung unter der Decke stattfindet - dh sie ist viel weniger offensichtlich als Ihre "genauso schlechten" Beispiele oben. Aus diesem Grund schien es plausibel, die zusätzliche Regel zur Verlängerung der Lebensdauer nur für die neue for-Schleife vorzuschlagen.
Ndkrempel
1
@ndkrempel: Ja, aber wenn Sie eine Sprachfunktion vorschlagen, um dies zu beheben (und daher mindestens bis 2017 warten müssen), würde ich es vorziehen, wenn sie umfassender wäre, etwas, das das Problem der vorübergehenden Erweiterung überall lösen könnte .
Nicol Bolas
3
+1. Beim letzten Ansatz deletekönnten Sie keine alternative Operation bereitstellen, die einen r-Wert zurückgibt: Vector normalize() && { normalize(); return std::move(*this); }(Ich glaube, dass der Aufruf normalizeinnerhalb der Funktion zur Überlastung des Werts führt, aber jemand sollte dies überprüfen :)
David Rodríguez - dribeas
3
Ich habe dies &/ &&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.
Christian Rau
1
@ChristianRau: Es ist neu in C ++ 11 und analog zu C ++ 03 "const" und "volatile" Qualifikationen nicht statischer Elementfunktionen, da es "this" in gewissem Sinne qualifiziert. g ++ 4.7.0 unterstützt es jedoch nicht.
Ndkrempel
25

for (double x: Vector {1., 2., 3.}. normalize ()) {...}

Dies ist keine Einschränkung der Sprache, sondern ein Problem mit Ihrem Code. Der Ausdruck Vector{1., 2., 3.}erstellt eine temporäre, aber die normalizeFunktion 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.

David Rodríguez - Dribeas
quelle
1
Würde eine constReferenz in diesem Fall die Lebensdauer des Objekts verlängern?
David Stone
5
Das würde die klar gewünschte Semantik 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.
Andy Ross
2
@AndyRoss: Warum? Jede temporäre Bindung an eine r-Wert-Referenz (oder const&) hat eine verlängerte Lebensdauer.
Nicol Bolas
2
@ndkrempel: Dennoch, keine Einschränkung der bereichsbasierten for-Schleife, würde das gleiche Problem auftreten, wenn Sie an eine Referenz binden : 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 Ausdruck Vector v = Vector{1., 2., 3.}.normalize().negate();erstellt zwei Objekte ...
David Rodríguez - dribeas
1
@ DavidRodríguez-dribeas: Das Problem bei der Bindung an const-reference ist folgendes: 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 das T const& f(T const& t) { return t; }und weint ... Wenn man operator+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.
Matthieu M.
4

IMHO ist das zweite Beispiel bereits fehlerhaft. Dass die modifizierenden Operatoren zurückkehren, *thisist 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 sehe

Vector v{1., 2., 3.};
auto foo = somefunction1(v, 17);
auto bar = somefunction2(true, v, 2, foo);
auto baz = somefunction3(bar.quun(v), 93.2, v.qwarv(foo));

Ich würde nicht automatisch vermuten, dass sich die Funktionen vals Nebeneffekt ändern . Natürlich könnten sie , aber es wäre verwirrend. Wenn ich also so etwas schreiben würde, würde ich sicherstellen, dass vdas konstant bleibt. Für Ihr Beispiel würde ich kostenlose Funktionen hinzufügen

auto normalized(Vector v) -> Vector {return v.normalize();}
auto negated(Vector v) -> Vector {return v.negate();}

und dann schreiben Sie die Schleifen

for( double x : negated(normalized(v)) ) { ... }

und

for( double x : normalized(Vector{1., 2., 3}) ) { ... }

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.

links herum
quelle
Vielen Dank. Wie immer gibt es viele Möglichkeiten. Eine Situation, in der Ihr Vorschlag möglicherweise nicht realisierbar ist, besteht darin, dass Vector beispielsweise ein Array (kein Heap zugewiesen) mit 1000 Doppelwerten ist. Ein Kompromiss zwischen Effizienz, Benutzerfreundlichkeit und Sicherheit.
Ndkrempel
2
Ja, aber es ist sowieso selten nützlich, Strukturen mit einer Größe> ≈100 auf dem Stapel zu haben.
links um