std::move einsetzen



  • 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:

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

    1. 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)?
    2. Okay, man darf nicht "Kein Ref-Qualifizierer" und "Ref-Qualifizierer" mischen, wohl aber LRef- und RRef-Qualifizierer?
    3. 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?


  • Mod

    Eisflamme schrieb:

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

    1. Okay, man darf nicht "Kein Ref-Qualifizierer" und "Ref-Qualifizierer" mischen, wohl aber LRef- und RRef-Qualifizierer?

    (Offensichtlich) richtig.

    Eisflamme schrieb:

    1. 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: move

    Fü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: move

    Im 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.


  • Mod

    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).


  • Mod

    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.


  • Mod

    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?


  • Mod

    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...).


  • Mod

    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 von return (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?


Anmelden zum Antworten