std::move einsetzen
-
Na ja, man kann Zeiger ja eben nutzen, wenn man ausdrücken will: Entweder, hier ist eine Referenz (Zeiger gesetzt) oder ich spare mir das Argument (Zeiger == nullptr). Da wäre es für mich ja auch sinnvoll, dass es einen rvalue-Zeiger gibt, der nullptr sein kann oder dereferenziert eben ein prvalue oder xvalue sein kann, genau so wie das ja auch eine RValue-Referenz anzeigen kann.
Den gleichen Effkt erzielt man allerdings durch explizites move nach der Dereferenzierung, ein solcher Typ wäre also ziemlich überflüssig.
Aber ein expliziter Move würde ja z.B. nicht einschließen, dass eine rValue-Übergabe auch eine LValue-Referenz sein kann, was aber eine RValue-Referenz auch ausdrücken kann (wie bei Perfect Forwarding eben...).
-
Eisflamme schrieb:
Aber ein expliziter Move würde ja z.B. nicht einschließen, dass eine rValue-Übergabe auch eine LValue-Referenz sein kann, was aber eine RValue-Referenz auch ausdrücken kann (wie bei Perfect Forwarding eben...).
Verstehe ich nicht. Ein solcher fiktiver rvalue-Zeiger wäre immer noch ein ganz normaler Zeiger, das übergebene Funktionsargument also in jedem Fall ein rvalue (ggf. nach l- zu rvalue-Konvertierung).
-
Eisflamme schrieb:
Und kann man die const&/&&-Überladung mit einer Methode hinbekommen, also das mit Perfect-Forwarding lösen? Also so was wie:
std::string&& flip(std::string&& text) { std::reverse(begin(std::forward(text)), end(std::forward(text))); return std::forward(text); }
oder so?
Oh nee ... Das ist völliger Quatsch.
1. string&& is not the return type you are looking for!
2. Die Signatur hat mit perfect forwardings nichts zu tun.
3. Bei std::forward fehlt noch ein Template-Argument.
4. std::begin/std::end geben im Falle eines Rvalue strings.const_iterator
en zurück.
5. Wo soll denn das neue Objekt herkommen, wenn das Argument ein Lvalue war?Die Perfect-Forwarding-Lösung würde so aussehen:
template<class T> // <-- das brauchen wir für perfect forwarding! typename enable_if< // <-- das ist sinnvoll um dieses Template auf strings... is_convertible<T&&,string>::value // ...zu beschraenken. string>::type flip(T && text) { string result = forward<T>(text); // <-- so ruft man std::forward auf reverse(begin(result),end(result)); return result; // NRVO anwendbar }
Wenn ich mich da nicht vertue, kann man sich damit im Falle eines Xvalue-Arguments (also ein Rvalue, was kein temporäres Objekt ist) gegenüber
string flip(string text) { reverse(begin(text),end(text)); return text; }
noch ein Move sparen (unter der Annahme, dass auch NRVO zum Einsatz kommt). Da spare ich mir aber lieber den Template-Bloat und habe eine lesbare, normale Funktion.
Merk dir einfach, dass
- ein Ausdruck nicht nur einen Typ hat sondern auch eine Wert-Kategorie. Die Wert-Kategorien, die man überladungsauflösungs-technisch unterscheiden kann sind Lvalues und Rvalues.
- Rvalues sind die Dinger, die man einfach so verändern darf, ohne dass das jemanden stören sollte, weil der Ausdruck sich entweder auf ein frisches, temporäres Objekt bezieht, oder weil jemand explizit ein Lvalue in ein Rvalue (genauer: Xvalue) umgewandelt hat und damit praktisch sagt "Der Zustand dieses Objekts ist mir nicht mehr wichtig".
- das wichtigste Einsatzgebiet für Überladungen nach der Wert-Kategorie die Copy/Move-Konstruktoren und Zuweisungsoperatoren sind. Hiermit definiert der Klassen-Autor genau, was "kopieren" und was "moven" vom Verhalten her bedeutet. Es ist also keine Compiler-Magie. Eine Rvalue-Referenz bekommt man nicht mit einem Lvalue des gleichen Typs initialisiert. Man kann sich also sicher sein, dass das, worauf sich eine Rvalue-Referenz bezieht, verändert werden darf.
- wenn du eine Variable mit einem Ausdruck initialisierst, der denselben Typ hat und sich auf ein temporäres Objekt bezieht, dann darf der Compiler das so optimieren, dass dieses "temporäre Objekt" direkt zu dem wird, worauf sich die Variable bezieht (Quelle und Ziel verschmelzen praktisch). So etwas ähnliches gibt es auch bei
return x;
, wobei x ein temporäres Objekt oder sogar eine funktionslokale Variable ist, die ja auch nicht meht benötigt wird. Damit spart man unnötige Kopien und Moves. Das nennt sich allgemein "copy/move elision" und im speziellen Fall vonreturn
(N)RVO, was für (named) return value optimization steht. - Spätestens wenn man && (ich habe bewusst nicht Rvalue-Referenz dazu gesagt) mit Templates kombiniert, sollte man noch zwei andere Regeln kennen. Das eine ist "Reference Collapsing". Reference-Collapsing spielt dann eine Rolle, wenn man eine Referenz auf eine Referenz gebildet hätte. Beispiel:
T&&
mit T=int& (T ist ein typedef/alias oder Template-Parameter). Reference Collapsing sorgt dafür dass die zwei aufeinander folgende Referenzen zu einer zusammen fallen, wobei sich eine Lvalue Referenz "durchdrückt". Ist also eine der beiden Referenzen eine Lvalue-Referenz, so ist der resultierende Typ auch eine Lvalue-Referenz, sonst eben eine Rvalue-Referenz. Im obigen Beispiel mit T=int& ergibt das also T&&=int&.T&&
sieht also nach einem Rvalue-Referenztyp aus, ist aber ein Lvalue-Referenztyp, weil T ein Lvalue-Referenztyp ist. - Die zweite "Template+&&"-Regel ist eine Sonderregel für die Template-Argument-Deduction. Schreibst du ein Funktions-Template, was einen Funktionsparameter vom Typ T&& entgegen nimmt, wobei T vom Compiler deduziert wird, dann wird T zu einer Lvalue-Referenz deduziert genau dann wenn das Funktionsargument ein Lvalue war. Der Parameter vom Typ T&& wird dadurch auch eine Lvalue-Referenz. Diese Funktion frisst dann also alles, Lvalues und Rvalues, wobei die Information, ob es ein Lvalue oder Rvalue war, noch im Template-Parameter T drinsteckt, weil es ja entweder ein Lvalue-Referenztyp oder ein normaler Objekt-Typ war. Diese letzten beiden Regeln sind die, die wir für "perfect forwarding" brauchen.
-
Hi,
nochmals vielen Dank für die super Antworten!! Tut mir echt Leid, dass ich euch so viel Zeit abverlange. Ohne Nachfragen würde ich es vermutlich nicht (oder nur nach tage/wochenlangem Nachdenken) verstehen.
So langsam lichten sich die Nebel. Ich lese das alles immer wieder durch und alle paar Male verstehe ich mehr Details. Braucht wahrscheinlich bei mir einfach etwas Zeit.
camper:
Okay, ein Zeiger ergibt wohl wenig Sinn. Ich würde halt gerne die Optionalität eines Parameters (die ich bei einer lvalue-Referenz über einen Zeiger löse) mit && (und Perfect Forwarding) verbinden. Wenn nullptr kommt, soll nichts passieren, wenn aber etwas kommt, soll er das je nach Typ automatisch das beste Verhalten auswählen. Kann sein, dass das in der Syntax überhaupt keinen Sinn ergibt. Überladung mit einer Funktion mit dem Parameter und einmal ohne ist wohl die schlauere Idee.krümelkacker:
Oh nee ... Das ist völliger Quatsch.
Sehe ich jetzt auch, find die Idee das gepostet zu haben darum super.
Eine Frage fällt mir noch direkt ein:
Schreibst du ein Funktions-Template, was einen Funktionsparameter vom Typ T&& entgegen nimmt, wobei T vom Compiler deduziert wird, dann wird T zu einer Lvalue-Referenz deduziert genau dann wenn das Funktionsargument ein Lvalue war.
Bringt das noch irgendeine Information dazu, die nicht sowieso durch Reference Collapsing impliziert ist? Wahrscheinlich steckt hier gerade noch ein Detail drin (also im ganzen Absatz), ich finde es aber noch nicht (oder ich habe wieder irgendeinen Logikfehler).
Und bei expliziter Typangabe statt Deduktion wäre die Aussage ja trotzdem noch korrekt, oder? (aber wahrscheinlich nicht erwähnenswert, weil das eh klar ist?)
Da muss man aber echt höllisch aufpassen und das Verständnis basiert hier auch auf sehr viel Liebe zum Detail. Ist das einfach so oder denke ich vielleicht falsch?
-
krümelkacker schrieb:
Oh nee ... Das ist völliger Quatsch.
Noe. Sieht mir voellig legitim aus.
krümelkacker schrieb:
1. string&& is not the return type you are looking for!
Doch.
krümelkacker schrieb:
2. Die Signatur hat mit perfect forwardings nichts zu tun
Korrekt. Hier braucht man std::move.
krümelkacker schrieb:
4. std::begin/std::end geben im Falle eines Rvalue strings
const_iterator
en zurück.'text' ist ein LValue.
krümelkacker schrieb:
5. Wo soll denn das neue Objekt herkommen, wenn das Argument ein Lvalue war?
Der Overload funktioniert eben nur mit RValues.
Edit: NVM, ging um perfect forwarding. War zur voreilig mit antworten.
Edit 2: Um noch was sinnvolles in diesem Post zu schreiben: Meines wissens nach ist ein Move beim returnen einer lokalen Variable garantiert, wenn T einen move ctor hat. Nix RVO/NRVO also.
-
Eisflamme schrieb:
Eine Frage fällt mir noch direkt ein:
Schreibst du ein Funktions-Template, was einen Funktionsparameter vom Typ T&& entgegen nimmt, wobei T vom Compiler deduziert wird, dann wird T zu einer Lvalue-Referenz deduziert genau dann wenn das Funktionsargument ein Lvalue war.
Bringt das noch irgendeine Information dazu, die nicht sowieso durch Reference Collapsing impliziert ist? Wahrscheinlich steckt hier gerade noch ein Detail drin (also im ganzen Absatz), ich finde es aber noch nicht (oder ich habe wieder irgendeinen Logikfehler).
Wenn ich dir Frage richtig verstehe, ist dir nicht klar, wozu es diese Deduktionsregel gibt. In C++11 sieht es aktuell mit dieser Regel so aus:
template<class T> void foo(T&& x); int main() { int i = 1729; foo(1234); // T=int, T&&=int&&, x ist eine Rvaluereferenz, alles ok foo(i); // T=int&, T&&=int&, x ist eine Lvaluereferenz, alles ok }
Würdest du diese Deduktionsregel rausschmeißen, dann hättest du auch bei foo(i) nur T=int und x wäre damit eine Rvaluereferenz. x kannst du aber dann nicht mit i initialisieren. Das ist nicht erlaubt. Du hättest dann einen Compile-Fehler. Du könntest zwar immer noch foo<int&>(i) schreiben, was dann Dank Reference Collapsing auch funktioniert. Aber es ist ja gerade der Witz an Perfect Forwarding, dass man keine Templateparameter explizit angeben muss.
Eisflamme schrieb:
Und bei expliziter Typangabe statt Deduktion wäre die Aussage ja trotzdem noch korrekt, oder? (aber wahrscheinlich nicht erwähnenswert, weil das eh klar ist?)
Da die obere Aussage sich auf Deduktion bezieht und du die Deduktion per expliziter Angabe übergehst, sehe ich nicht, wie ich sinnvoll auf diese Frage antworten kann. Du kannst aber natürlich T manuell wählen, z.B.
foo<const int&>(1234); // T=const int&, T&&=const int&, // foo::x ist eine Lvaluereferenz auf const
Aber wenn es sich vermeiden lässt, sollte man lieber die Finger von expliziten Template-Argumenten bei Funktionstemplates lasen.
Eisflamme schrieb:
Da muss man aber echt höllisch aufpassen und das Verständnis basiert hier auch auf sehr viel Liebe zum Detail. Ist das einfach so oder denke ich vielleicht falsch?
Nee, ist leider ein kompliziertes Thema. Habe auch lange gebraucht, das zu verstehen.
-
Ja, aber als Überladung für RValue-Referenzen wäre die Funktion doch auch Käse, oder? Ich meine, move statt forward ist doch genauer (wobei forward auch klappen sollte, wenn das Template-Argument hinzukommt, richtig?).
Und da fällt mir noch eine Frage ein: Wieso braucht forward denn eine explizite Typangabe? Wenn die Funktion T ohne & und && braucht, könnte sie das doch selbst über remove_reference rausfinden oder nicht?
-
Eisflamme schrieb:
Und da fällt mir noch eine Frage ein: Wieso braucht forward denn eine explizite Typangabe? Wenn die Funktion T ohne & und && braucht, könnte sie das doch selbst über remove_reference rausfinden oder nicht?
Benannte RValue Referenzen sind LValues.
-
Kellerautomat schrieb:
krümelkacker schrieb:
1. string&& is not the return type you are looking for!
Doch.
Nö. Das Recycling von temporären Objekten ist zwar löblich aber die Information, dass es sich um ein kurzlebiges Objekt handelt, geht mit diesem Rückgabetyp verloren, so dass du z.B. hier unedfiniertes Verhalten hervorrufen würdest
for (auto c : flip("hello world")) { cout << c; }
weil der temporäre String hier schon zu früh zerstört würde. Die Iteratoren holt sich das for-range hier von einer baumelnden Referenz ==> U.B.
Kellerautomat schrieb:
krümelkacker schrieb:
4. std::begin/std::end geben im Falle eines Rvalue strings
const_iterator
en zurück.'text' ist ein LValue.
Er schrieb std::begin(std::forward(text)). Die richtige Anwendung von forward hätte hier ein Rvalue geliefert.
Kellerautomat schrieb:
krümelkacker schrieb:
5. Wo soll denn das neue Objekt herkommen, wenn das Argument ein Lvalue war?
Der Overload funktioniert eben nur mit RValues.
Da es eine perfect forward lösung sein wollte, hätte sie auch Lvalues geschluckt, die dann irrtümlicherweise verändert würden, obwohl der Aufrufer das gar nicht möchte...
Kellerautomat schrieb:
Edit 2: Um noch was sinnvolles in diesem Post zu schreiben: Meines wissens nach ist ein Move beim returnen einer lokalen Variable garantiert, wenn T einen move ctor hat. Nix RVO/NRVO also.
Es wird in diesem Fall nur gemoved, wenn der Compiler zu dämlich ist, NRVO durchzuführen. Bei pass-by-value Funktionsparametern hast du aber recht, da dort NRVO nicht mehr erlaubt ist. Diesen Fall konnte eh kein Compiler optimieren.
-
Kellerautomat schrieb:
Eisflamme schrieb:
Und da fällt mir noch eine Frage ein: Wieso braucht forward denn eine explizite Typangabe? Wenn die Funktion T ohne & und && braucht, könnte sie das doch selbst über remove_reference rausfinden oder nicht?
Benannte RValue Referenzen sind LValues.
Dementsprechend wüsste forward nicht, ob du eine benannte Lvalue- oder benannte Rvalue-Referenz reinsteckst, weil beide Ausdrücke Lvalues sind.
Aber ich habe neulich das hier gesehen:
#define FORWARD(x) std::forward<decltype(x)>(x)
Ob ich das toll finden soll, weiß ich aber noch nicht.
-
krümelkacker schrieb:
Nö. Das Recycling von temporären Objekten ist zwar löblich aber die Information, dass es sich um ein kurzlebiges Objekt handelt, geht mit diesem Rückgabetyp verloren, so dass du z.B. hier unedfiniertes Verhalten hervorrufen würdest
for (auto c : flip("hello world")) { cout << c; }
weil der temporäre String hier schon zu früh zerstört würde. Die Iteratoren holt sich das for-range hier von einer baumelnden Referenz ==> U.B.
Noe. Die Lebenszeit des Objekts wird in den Scope der Schleife verlaengert.
krümelkacker schrieb:
Kellerautomat schrieb:
Korrekt. Hier braucht man std::move.
So ein Quatsch.
Natuerlich tut man das.
So ein Quatsch.krümelkacker schrieb:
Kellerautomat schrieb:
'text' ist ein LValue.
Er schrieb std::begin(std::forward(text)). Die richtige Anwendung von forward hätte hier ein Rvalue geliefert.
Die beiden forwards hab ich in der Tat uebersehen.
krümelkacker schrieb:
Kellerautomat schrieb:
krümelkacker schrieb:
5. Wo soll denn das neue Objekt herkommen, wenn das Argument ein Lvalue war?
Der Overload funktioniert eben nur mit RValues.
...und mutiert irrtümlicher weise Lvalue-Argumente obwohl der Aufrufer das gar nicht möchte...
Und du behauptest jetzt ernsthaft, dass dieses Fuktionstemplate keinen Quatsch ist? Wow!
Der Aufruf mutiert gar nichts, wenn er mit LValues *nicht kompiliert*. Und wo du hier ein Template siehst, weiss ich nicht.
-
Hi,
krümelkacker:
Zu Antwort 1)
Okay, alles verstanden, meine Frage war falsch gestellt, weil ich den Fall, den Du mit Deinem Code skizziert hast, gar nicht gesehen hatte. Die darauf folgende Frage erübrigt sich dann natürlich.Nee, ist leider ein kompliziertes Thema. Habe auch lange gebraucht, das zu verstehen.
Beruhigend zu wissen.
Zu Antwort 2)
krümelkacker schrieb:
Kellerautomat schrieb:
Eisflamme schrieb:
Und da fällt mir noch eine Frage ein: Wieso braucht forward denn eine explizite Typangabe? Wenn die Funktion T ohne & und && braucht, könnte sie das doch selbst über remove_reference rausfinden oder nicht?
Benannte RValue Referenzen sind LValues.
Dementsprechend wüsste forward nicht, ob du eine benannte Lvalue- oder benannte Rvalue-Referenz reinsteckst, weil beide Ausdrücke Lvalues sind.
Okay, das hat bei mir lange gedauert, weil ich kurz vergessen hatte, dass T ja auch wieder Wert-Kategorien beinhaltet, aber jetzt hab ich das gerafft, super.
-
Kellerautomat schrieb:
Noe. Die Lebenszeit des Objekts wird in den Scope der Schleife verlaengert.
Nicht, wenn dein flip eine Rvaluereferenz zurück gibt! Guck mal nach, wie for-range im Standard definiert ist und in welchen Fällen die Lebenszeit eines Objekts an die der gebundenen Referenz angepasst wird.
Vielleicht glaubst du ja auch diesem Test-Programm. Was dir auffallen sollte ist, dass im zweiten Fall das Objekt zerstört wird bevor die Ausgabe stattfindet.
Referenzen auf etwas zurück geben, was sich womöglich auf ein temporäres Objekt bezieht ist eher eine schlechte Idee, wie man sehen kann. Nicht ohne Grund wurde ein früherer C++ Standard Entwurf korrigiert, in dem
string&& opeartor+(string&& lhs, string const& rhs); string&& opeartor+(string const& lhs, string&& rhs); string&& opeartor+(string&& lhs, string&& rhs);
durch
string opeartor+(string&& lhs, string const& rhs); string opeartor+(string const& lhs, string&& rhs); string opeartor+(string&& lhs, string&& rhs);
ersetzt wurde.
Kellerautomat schrieb:
krümelkacker schrieb:
Kellerautomat schrieb:
Korrekt. Hier braucht man std::move.
So ein Quatsch.
Natuerlich tut man das.
So ein Quatsch.Das sollte doch eine perfect forwarding lösung sein!
Kellerautomat schrieb:
Der Aufruf mutiert gar nichts, wenn er mit LValues *nicht kompiliert*. Und wo du hier ein Template siehst, weiss ich nicht.
Das sollte doch eine perfect forwarding lösung sein!
-
Kann es sein dass ihr euch hier an der perfect forwarding Definition aufhaltet?
- Kein move/copy mehr als nötig
vs
- Type-Deduction