RValue references und templates



  • Hallo,

    folgender code:

    #include <string>
    #include <iostream>
    
    template< class Type >
    void f( const Type &Object )
    {
    	std::cout<< "const Type &" <<std::endl;
    }
    
    template< class Type >
    void f( Type &&Object )
    {
    	std::cout<<"Type &&"<<std::endl;
    	Type temp = Type( std::move( Object ) );
    	temp += " xxxxx ";
    	std::cout<< temp <<std::endl;
    }
    
    int main()
    {
    	std::string str = "Hi";
    	f( str);
    	f( str);
    	std::string end;
    	std::cin>>end;
    	return 0;
    }
    

    Ich habe jetzt erwartet, dass beim ersten Mal f( input ) die "const Type &" Überladung aufgerufen wird, beim zweiten Mal entweder "Type &&" oder, falls der Compiler es nicht bemerkt, dass ich "str" danach nicht mehr verwende, nochmal "const Type &".
    Der gcc 4.6 will das gar nicht compilen:

    ..\Source\main.cpp: In function 'void f(Type&&) [with Type = [b]std::basic_string<char>&[/b]]':
    ..\Source\main.cpp:77:9:   instantiated from here
    ..\Source\main.cpp:64:40: error: invalid cast of an rvalue expression of type 'std::remove_reference<std::basic_string<char>&>::type' to type 'std::basic_string<char>&'
    

    Mit was genau will er hier die move-Version aufrufen - mit einer Referenz auf ein RValue?
    Also sowas wie "Type &&&"?!

    Mit VC++2010 compiled das ganze, Ausgabe:

    Type &&
    Hi xxxxx
    Type &&
    Hi xxxxx xxxxx
    

    Auch hier wird also zweimal die move-Version aufgerufen, als hätte ich sowas wie eine Referenz übergeben...
    Welcher compiler hat Recht? Hat überhaupt einer Recht? Ist ja noch quasi beta-Phase was c++0x angeht.
    Wenn ja: Wie kann man das Verhalten erklären?



  • Die Erklärung dürfte sein, dass die Zeile

    Type temp = Type( std::move( Object ) );
    

    als

    std::string &temp = (std::string&)(std::move(Object));
    

    verstanden wird.

    Was Type &&& angeht...8.3.2 (5) in n3290 scheint mir zu besagen, dass eine rvalue-Referenz auf eine lvalue-Referenz nicht existieren können sollte, aber wenn sowohl gcc als auch msvc den Teil gleich machen (der Unterschied ist ja nur, dass gcc den Cast ablehnt), möchte ich mich nicht so weit aus dem Fenster lehnen, zu behaupten, dass ich das besser weiß als zwei erfahrene Teams von Compilerbauern.



  • Lies dich mal Schlau was Template Type Deduction und Rvalue-References angeht.
    Ich glaube bei einem Template mit T&& kann T zu U& deduced werden, und T&& bei T==U& ergibt im Endeffekt wieder U&.

    Anders gesagt: "Object" in deiner Template Funktion ist nicht unbedingt eine Rvalue Referenz.

    Warum er das 2. Template als besseren Match ansieht weiss ich nicht, vermutlich wegen des const im 1. Template.

    Leider kenne ich mich mit Rvalue-Referenzen aber noch nicht ausreichend aus, um dir hier was definitives sagen zu können.

    EDIT: aaaaaaah, was seldon sagt macht Sinn 🙂
    Versuch mal bei MSVC die Microsoft-Extensions abzudrehen. MSVC erlaubt mit Extensions nämlich Rvalues an normale Lvalue-Referenzen zu binden. Vielleicht erlaubt er dann auch Rvalue-Referenzen an Lvalue-Referenzen zu binden.



  • Der zweite Overload wird bevorzugt aufgrund des consts - soweit richtig.

    Was das binden von Referenzen angeht:
    T& + U& = U&
    T&& + U& = U&
    T&& + U&& = U&&

    Dadurch wird dein Parameter eine (non-const) LValue Referenz auf std::string.

    Edit: @Frage MSVC hat Recht.
    Edit vom Edit: Ach Gottchen, ich bin zu müde und sollte um die Zeit nicht mehr antworten. Der GCC hat natürlich Recht, Type ist ein std::string& und std::move gibt einen std::string&& zurück - das kann nicht funktionieren.



  • Zwei Denkfehler:

    (1) Das T in "T&&Object" wird bei dir durch string& ersetzt. Objekt ist damit eine non-const Lvalue-Referenz und die Funktion ist damit ein besserer Match. Hier kommt eine Sonderregel zum Einsatz, die "Perfect Forwarding" ermöglicht. Merke Dir, dass die "Perfect-Forwarding-Signatur"

    template<class T>
    void foo(T&&)
    

    ein Funktionstemplate erzeugt, was alles frisst. T wird ggf ein Lvalue-Referenz-Typ sein. Dann ist auch T&& ein Lvalue-Referenz-Typ wegen "reference collapsing", also quasi & + && = &.

    (2) Der Compiler darf auch im zweiten Fall "str" nicht als Rvalue behandeln -- unabhängig davon, ob er schlau genug ist, festzustellen, dass str nicht mehr benutzt wird. Du musst also explizit std::move an der Stelle benutzen. Die einzige Stelle, bei der sich der Compiler über die "Lvalueness" hinwegsetzen darf und muss ist im return-Statement, wenn ein funktionslokales Objekt zurückgegeben wird. Beispiel:

    string flip(string x)
    {
      reverse(x.begin(),x.end());
      return x;
    }
    

    x bezieht sich auf ein funktionslokales Objekt. Da string einen move-ctor bekommt, wird dieser hier verwendet, um das "return-objekt" zu konstruieren. Dieser ctor wird erwartungsgemäß den "Inhalt" von x klauen.



  • Danke, Perfect Forwarding war genau das richtige Stichwort zu googlen 🙂
    Das Tutorial, das ich zu RValues gelesen hatte, behandelt dieses Thema leider nicht.


Anmelden zum Antworten