std::move einsetzen
-
Das hätte man früher mit Referenz-auf-const gemacht und man hätte explizit eine Kopie erzeugt. Hier wird nirgenswo etwas explizit kopiert und das ist gut so.
string flip(string const& text) { string kopie = test; // <-- hier std::reverse(begin(kopie),end(kopie)); return kopie; }
Wieso text nicht direkt als Kopie uebergeben? Welchen Sinn hat denn die Uebergabe als Referenz, wenn man den String intern sowieso veraendern muss.
-
Wieso text nicht direkt als Kopie uebergeben? [...]
Das ist sowieso eine bescheuerte Methode.
string flip(string const& text) { return string(text.rbegin(), text.rend()); }
Edit: Das wird hier bestimmt so stark optimiert, dass es im Endeffekt sogar nicht langsamer sein dürfte als eine ganz normale Kopie.
Edit²: Geprüft, und leider ist es nicht der Fall.flip
ist 1,5 bis 2 mal Langsamer als eine Kopie...
Edit³: Ahh, aber es ist gleichauf mit einer normalen Start-End-Iteratorenpaar Kopie.
-
Ferris schrieb:
Wieso text nicht direkt als Kopie uebergeben? Welchen Sinn hat denn die Uebergabe als Referenz, wenn man den String intern sowieso veraendern muss.
In C++03 hätte ich
string flip(string x) { std::reverse(x.begin(),x.end()); return x; // Kein NRVO, aber immerhin ein Move ab C++11 }
nicht geschrieben (in C++11 aber schon!), weil kein C++ Compiler in diesem Fall NRVO durchführen kann. Und weil std::string vor C++11 keinen Move-Konstruktor hatte, wird der return-Wert der Funktion immer von x kopiert. Das ist jetzt nicht besser als
string flip(string const& c) { string x = c; std::reverse(x.begin(),x.end()); return x; // NRVO anwendbar! }
weil hier NRVO für x greift. Die optimale Lösung, die in C++03 in allen Situationen bei einen gescheiten Compiler die Zahl der dynamischen Speicherallozierungen minimiert sieht tatsächlich so aus:
string flip(string c) { string x; // Annahme: Default-Ctor kostet praktisch nichts x.swap(c); // Annahme: swap kostet praktisch nichts std::reverse(x.begin(),x.end()); return x; // NRVO anwendbar! }
Hier wird weder explizit kopiert noch NRVO ausgehebelt. Das Objekt
c
kann ggf durch per copy-elision erzeugt werden. Hier werden alle copy-elision Tricks ausgenutzt, die vom Standard erlaubt und von Compilern tatsächlich unterstützt werden.Sone schrieb:
Wieso text nicht direkt als Kopie uebergeben? [...]
Das ist sowieso eine bescheuerte Methode.
string flip(string const& text) { return string(text.rbegin(), text.rend()); }
Ich hoffe, dass das nur dein C++03-Ansatz ist; denn sonst würdest du die move-Optimierungen von string gar nicht ausnutzen. Aber in C++03 hättest du hier noch copy-elision für den Parameter ausnutzen können. Copy-Elision für Parameter zusammen mit RVO ist möglich (siehe swap-Trick).
-
Kurze Nachfrage:
Bei (trivialen) Gettern sollte man dann also auch nicht mehr mit const Typ& als Rückgabetyp arbeiten, sondern einfach nur Typ schreiben, wenn Typ moveable ist? Oder ist das egal, weil so oder so eben genau eine Kopie gemacht wird?
-
Eisflamme schrieb:
Bei (trivialen) Gettern sollte man dann also auch nicht mehr mit const Typ& als Rückgabetyp arbeiten, sondern einfach nur Typ schreiben, wenn Typ moveable ist?
Nö, nicht unbedingt.
Eisflamme schrieb:
Oder ist das egal, weil so oder so eben genau eine Kopie gemacht wird?
Egal ist das auch nicht. Der Nutzer will den String vielleicht nur ausgeben/untersuchen und braucht daher keine eigene Kopie
-
Oh... ist natürlich richtig. Danke!
-
Auch kann man folgendes machen:
struct foo { std::string const& s() const & { return s_; } std::string && s() && { return std::move(s_); } private: std::string s_; };
Das funktioniert dann auch mit Temporaries richtig.
-
Aber bei einem non-const Get der Klasse ihren s_-Inhalt zu klauen ist doch sicher nicht beabsichtigt? Oder missverstehe ich was? Ich dachte, && sei nur eine rvalue-Referenz, wieso benötigt es dann einen Move?
Und die Syntax:
`string&& func()
**&&**
{return move(s_);}` (also mit den && nach Funktionsnamen) habe ich auch noch nie gesehen, ist && mittlerweile auch ein Qualifizierer? Vielleicht muss ich mir einfach nochmal ein paar Artikel zu den rvalue-Referenzen durchlesen..
-
Eisflamme schrieb:
Ich dachte, && sei nur eine rvalue-Referenz, wieso benötigt es dann einen Move?
Aus demselben Grund, warum...
string blah = "hello"; string&& dies = blah; // nicht funktioniert (compile-fehler), string&& jenes = move(blah); // aber schon.
Beachte, dass hier gar nix "movt". move() ist nur ein rvalue-cast. Das Ergebnis von move bezieht sich immer noch auf dasselbe Objekt. Der Ausdruck ist aber kein Lvalue mehr sondern ein Rvalue. Hier wird das benötigt, weil man Rvalue-Referenzen nicht mit Lvalues initialisieren kann (es sei denn, da ist eine implizite Konvertierung dazwischen, so dass sich die Rvalue-Referenz auf ein neues temporäres Objekt beziehen würde).
Eisflamme schrieb:
ist && mittlerweile auch ein Qualifizierer?
Ja. Damit kannst du eben auch für Methoden zwischen L- und Rvalue unterscheiden. Das ging ja vorher nicht. Entweder haben die Überladungen alle einen Ref-Qualifizierer, oder sie haben alle keinen. Mischen darf man das nicht. Und falls es Ref-Qualifizierer gibt, sind die Überladungsauflösungsregeln genauso wie als wenn die Funktionen einen zusätzichen Referenz-Parameter "self" hätten, der entsprechend qualifiziert ist.
-
Hi,
ich habe nochmal ein paar Artikel/Forensachen gelesen und dadurch jetzt etwas mehr verstanden. Aber leider noch nicht alles, irgendwie geht das mehr in die Tiefe, als ich dachte.
Also die Varianten auf S.1 dieses Threads nutzen alle implizit aus, dass der Compiler optimieren kann, weil er RValue-Referenzen ausnutzen kann? Ohne Optimierung wäre der Code ja erstmal nicht besonders toll, weil der Parametertyp std::string ja immer eine Kopie erzeugt. Stimmt das so weit?
Dann habe ich mir nochmal diesen Code angeschaut:
struct foo { std::string const& s() const & { return s_; } std::string && s() && { return std::move(s_); } private: std::string s_; };
Die Erklärung ist wohl in diesem Zitat:
krümelkacker schrieb:
- Damit kannst du eben auch für Methoden zwischen L- und Rvalue unterscheiden. Das ging ja vorher nicht. 2) Entweder haben die Überladungen alle einen Ref-Qualifizierer, oder sie haben alle keinen. Mischen darf man das nicht. 3) Und falls es Ref-Qualifizierer gibt, sind die Überladungsauflösungsregeln genauso wie als wenn die Funktionen einen zusätzichen Referenz-Parameter "self" hätten, der entsprechend qualifiziert ist.
Da muss ich aber nochmal nachhaken:
- Heißt L/R-Value hier bezogen auf die Klasseninstanz (also wie auch der const-Qualifizierer der Methode sich ja eigentlich auf die Instanz richtet bzw. dafür sorgt, dass bei einer const-Instanz die const-Methodenüberladung aufgerufen wird)?
- Okay, man darf nicht "Kein Ref-Qualifizierer" und "Ref-Qualifizierer" mischen, wohl aber LRef- und RRef-Qualifizierer?
- Hier komm ich gerade nicht mit, was sind denn Überladungsauflösungsregeln? Den self-Parameter verstehe ich, glaube ich, der übernähme eben den Methoden-Qualifizierer als Parameter-Qualifizierer, genau so wie die Methoden-Qualifizierer ja eigentlich auch die Qualifizierer für das Objekt sind, richtig? Vermutlich drücke ich das zu umständlich aus, ich hoffe, man versteht, was ich meine (und dass es dasselbe ist, was Du meinst).
Und jetzt springe ich nochmal auf die erste Antwort zurück:
std::string flip(std::string text) { std::reverse(begin(text),end(text)); return text; }
[...]
Das kannst du eigentlich nur noch mit der const&/&&-Überladung toppen, weil dadurch noch ein Move gespart werden kann.Okay, also ohne die Überladung mit RValue "moved" er den RValue in text, reverse moved alles brav hin und her und er moved das Ergebnis von text wieder in den Empfänger zurück.
Und mit der &&-Überladung (also Parameter = std::string&& text) würde er in text nichts moven, weil text nur eine Referenz wäre, reverse würde genau brav alles hin- und hermoven, aber bei der Rückgabe erfolgt wieder der move. Also sparen wir den move bei Parameterübergabe, stimmt das so weit? Sparen wir auch in der const&-Variante (also für einen LValue) etwas mit der Überladung?
Und kann man die const&/&&-Überladung mit einer Methode hinbekommen, also das mit Perfect-Forwarding lösen? Also so was wie:
std::string&& reverse(std::string&& text) { std::reverse(begin(std::forward(text)), end(std::forward(text))); return std::forward(text); }
oder so?
-
Eisflamme schrieb:
- Heißt L/R-Value hier bezogen auf die Klasseninstanz (also wie auch der const-Qualifizierer der Methode sich ja eigentlich auf die Instanz richtet bzw. dafür sorgt, dass bei einer const-Instanz die const-Methodenüberladung aufgerufen wird)?
Genau. Gemeint ist der implizite Objektparameter der nichtstatischen Memberfunktion. Also das, was vor dem Punkt steht (oder -> - dann ist es immer ein lvalue, weil p->f als (*p)->f interpretiert wird).
Eisflamme schrieb:
- Okay, man darf nicht "Kein Ref-Qualifizierer" und "Ref-Qualifizierer" mischen, wohl aber LRef- und RRef-Qualifizierer?
(Offensichtlich) richtig.
Eisflamme schrieb:
- Hier komm ich gerade nicht mit, was sind denn Überladungsauflösungsregeln? Den self-Parameter verstehe ich, glaube ich, der übernähme eben den Methoden-Qualifizierer als Parameter-Qualifizierer, genau so wie die Methoden-Qualifizierer ja eigentlich auch die Qualifizierer für das Objekt sind, richtig? Vermutlich drücke ich das zu umständlich aus, ich hoffe, man versteht, was ich meine (und dass es dasselbe ist, was Du meinst).
Überladungsauflösung ist der letzte Schritt bei der Bestimmung der aufzurufenden Funktion bei einem Funktionsaufruf:
1. Zusammensuchen aller auffindbaren Deklarationen (Namelookup)
2. ggf. Bestimmen von nicht explizit angegeben Templateargumenten (Templateargumentdeduktion), ggf. Entfernen unpassender Deklarationen (SFINAE)
3. Auswahl der besten Funktion aus dem Set aller verbleibenden Deklarationen
(4. Zugriffskontrolle)Es sind im Prinzip 3 Fälle, die zu untersuchen sind:
1. Argument ist lvalue
2. Argument ist rvalue und ein temporäres Objekt (prvalue)
3. Argument ist rvalue aber kein temporäres Objekt (xvalue)Für
std::string flip(std::string text) { std::reverse(begin(text),end(text)); return text; }
ergibt sich
1. Aufruf: copy return: move
2. Aufruf: move(RVO) return: move
3. Aufruf: move return: moveFür
std::string flip(std::string&& text) { std::reverse(begin(text),end(text)); return std::move(text); } std::string flip(const std::string& s) { std::string text = s; std::reverse(begin(text),end(text)); return text; }
ergibt sich
1. Aufruf: - in Funktion: copy return: move(RVO)
2. Aufruf: - return: move
3. Aufruf: - return: moveIm Falle eines Aufrufes mit einem lvalue, wird also ein move eingespart (was im Vergleich zum verbleibenden Copy völlig irrelevant sein dürfte)
Im Falle eines Aufrufes mit einem rvalue, das kein temporäres Objekt darstellt, wird so nur ein move statt zweien durchgeführt. Das kann u.U. relevenat sein, im Regelfall allerdings vermutlich nicht.
-
Hi,
vielen Dank für die ausführliche Antwort, das hat schon eine ganze Menge für mich geklärt.
Ich sehe gerade nur nicht, wo wir eine Kopie bei lvalue-Aufruf sparen:
1. Aufruf: copy return: move
vs.
1. Aufruf: - in Funktion: copy return: move(RVO)
Vielleicht verstehe ich die Syntax dieser Beschreibungen auch nicht ganz, aber bis auf das (RVO) und das "- in Funktion" (verstehe auch nicht ganz diesen Zusatz) sieht das doch gleich aus.
-
Eisflamme schrieb:
Hi,
vielen Dank für die ausführliche Antwort, das hat schon eine ganze Menge für mich geklärt.
Ich sehe gerade nur nicht, wo wir eine Kopie bei lvalue-Aufruf sparen:
1. Aufruf: copy return: move
vs.
1. Aufruf: - in Funktion: copy return: move(RVO)
Vielleicht verstehe ich die Syntax dieser Beschreibungen auch nicht ganz, aber bis auf das (RVO) und das "- in Funktion" (verstehe auch nicht ganz diesen Zusatz) sieht das doch gleich aus.
Aufruf: -
bedeutet, dass keine Funktion aufgerufen werden muss, um das Funktionasargument an den formalen (Referenz-)Parameter zu binden (den Fall, dass das Argument kein string ist, brauchen wir hier nicht separat zu behandeln, dort wird einfach eine Konvertierungssequenz vorgeschaltet und man macht dann mit Fall2 (prvalue) weiter).in Funktion: copy
bedeutet, dass in der Funktion ein Aufruf des Kopierkonstruktors erfolgt (um text zu initialisieren).(RVO) bedeutet jeweils, dass der Compiler dieses move(copy) eleminieren darf (im Falle der const&-Überladung wäre NRVO präziser), und für unsere Zwecke gehen wir nat. davon aus, dass dies auch geschieht.
P.S. bei rvalues meine ich nat. modifizierbare rvalues - bei const-Qualifikation des Arguments wäre entsprechend zu kopieren statt zu moven (an der ggf. vorhanden Möglichkeit der Eleminierung ändert sich nichts).
-
Eisflamme schrieb:
Ich sehe gerade nur nicht, wo wir eine Kopie bei lvalue-Aufruf sparen:
1. Aufruf: copy return: move
Ich auch nicht. In C++03 war RVO für Funktionsargumente nicht ausdrücklich ausgeschlossen, kein realer Compiler hat es aber je gemacht. In C++11 ist RVO für Funktionsargumente nicht zulässig. Eine Kopie ist also im lvalue-Fall unvermeidlich.
Wenn es nicht gerade um std::string ginge, ist theoretisch noch ein (normalerweise pathologischer) 4. Fall zu berücksichtigen: prvalues, die ein abgeleitetes Klassenobjekt darstellen. Der Unterschied zu Fall 2 besteht aber auch nur darin, dass RVO beim Aufruf dann nicht möglich ist.
-
Okay, vielen Dank erstmal! Das meiste habe ich jetzt kapiert und mein Kopf dröhnt jetzt, aber das ist gut so. Ich lese das morgen nochmal (habe beim ersten Lesen jetzt schon das meiste verstanden, aber ich will nicht zu viel fragen, worauf ich evtl. doch noch mit eigenem Denken komme :))
Mir ist nur noch eine weitere Frage eingefallen, die ich gerne zusätzlich loswerden wollen würde:
Gibt es so etwas wie rvalue-Pointer? Oder anders gefragt: Manchmal ist bei mir ein Funktionsargument ein Zeiger, um anzudeuten, dass nullptr oder ein valides Argument in Odnung sind, sonst unterscheidet sich der Parameter aber nicht von einer Referenz. Das löst man dann hier wohl einfach mit Funktionsüberladung (einmal mit Parameter, einmal ohne), oder?
-
Nein, es gibt keine RValue Pointer. Wuerde ja auch nicht viel Sinn ergeben, denn ein Pointer repraesentiert die Adresse eines Objekts. Eine Temporary hat aber unter Umstaenden gar keine Adresse, weil sie z.b. in einem Register liegt.
-
Kellerautomat schrieb:
Nein, es gibt keine RValue Pointer. Wuerde ja auch nicht viel Sinn ergeben, denn ein Pointer repraesentiert die Adresse eines Objekts. Eine Temporary hat aber unter Umstaenden gar keine Adresse, weil sie z.b. in einem Register liegt.
Was ist denn ein Register?
-
Da ich weiss, dass du das weisst, stellt sich mir die Frage, warum du das hinterfragst? Ist es, dass der Standard keine Register kennt? Wenn nicht, was dann?
-
Kellerautomat schrieb:
Da ich weiss, dass du das weisst, stellt sich mir die Frage, warum du das hinterfragst? Ist es, dass der Standard keine Register kennt? Wenn nicht, was dann?
Unabhängig davon, ob der Standard das kennt oder nicht, ist es hier jedenfalls völlig deplatziert.
Theoretisch könnte man sich einen Zeigertyp vorstellen, der bei Dereferenzierung ein xvalue produziert. Den gleichen Effkt erzielt man allerdings durch explizites move nach der Dereferenzierung, ein solcher Typ wäre also ziemlich überflüssig.
-
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...).