Parameterübergabe R-Value und const-reference



  • Hallo @all,

    ich finde im Netz wiederkehrend widersprüchliche Aussagen welches der folgenden
    Lösungen mein Problem am besten löst.
    Zum Problem:
    Ich habe ein Thread, wobei seine Parameter beim Start kopiert bzw. verschoben werden müssen, da evtl. sonst die Lebenszeit des Threads, welcher diesen Thread startet, nicht lang genug ist. Um es dem Nutzer freizustellen, welche Version er nutzen möchte, benötige ich eine Version, die entscheidet, ob kopiert oder verschoben werden soll.

    /*
    	Mein erster Ansatz unterscheidet bei den Funktionen zwischen
    	const-reference und R-Value, wobei die const-reference eine Kopie
    	erzeugt und dann die R-Value Version mit dieser Kopie aufruft.
    	Problem:
    	Bei n Parametern benötige ich 2^n Funktionen
    */
    void foo( std::string&& )
    {
    	// Do Stuff
    }
    
    void foo( const std::string& s )
    {
    	foo( std::move( std::string( s ) ) );
    }
    
    /*
    	Hierbei sei es dem Nutzer überlassen, ob er mittels
    	std::move den Parameter übergibt oder eine Kopie-erstellt.
    	Problem:
    	Möchte ich den String in der Funktion nochmals verschieben o.Ä., also muss ich nochmals std::move 
    	aufrufen, ich habe also effektiv immer 1 move-Konstruktor Aufruf + 1 Move oder Copy Aufruf
    */
    void foo( std::string s )
    {
    	//Do Stuff
    }
    
    /*
    	Erscheint mit recht aufwendig und ich weiß nicht 100% ob ich das richtig
    	verwendet habe
    */
    template < class T,
               class = typename std::enable_if<std::is_is_convertible<T,std::string>::value>::type>
    void foo( T&& )
    {
    	// Do Stuff
    }
    

    Soweit meine Ansätze. Evtl. gibt es auch noch eine andere? Welche von den ist sonst die zu empfehlene?

    MfG

    shft



  • Nimm einfach die zweite Variante, ein move kostet dich irgendwie 4-8 instructions oder so. Jedenfalls voellig irrelevant in einem Kontext wo du gerade einen extra Thread startest. Ich wuerde die zweite Variante auch sonst fast ueberall nutzen, ausser du hast wirklich einen guten Grund es nicht zu tun.



  • Es gibt einen Vortrag von Herb Sutter da werden verschiedene Möglichkeiten diskutiert. Siehe hier, etwa ab Minute 55: https://www.youtube.com/watch?feature=player_detailpage&v=xnqTKD8uD64#t=3300



  • Danke, das Video ist echt sehr hilfreich 🙂



  • shft schrieb:

    Ich habe ein Thread, wobei seine Parameter beim Start kopiert bzw. verschoben werden müssen, da evtl. sonst die Lebenszeit des Threads, welcher diesen Thread startet, nicht lang genug ist. Um es dem Nutzer freizustellen, welche Version er nutzen möchte, benötige ich eine Version, die entscheidet, ob kopiert oder verschoben werden soll.

    Dafür brauchst du gar keine zwei Versionen. Das Kopieren/Moven machst nicht Du sondern der Nutzer bzw die thread-Library selbst. Der Parameter-Typ den, du für deine Funktion wählst, die du in einem neuen Thread ausführst, ist davon nahezu unabhängig.

    shft schrieb:

    void foo( const std::string& s )
    {
    	foo( std::move( std::string( s ) ) );
    }
    

    Das std::move ist hier überflüssig, da std::string(s) ja eh schon ein Rvalue ist.

    shft schrieb:

    /*
    	Problem:
    	Möchte ich den String in der Funktion nochmals verschieben o.Ä.,
    	also muss ich nochmals std::move [...]
    	*/
    

    Du willst also einen eigenen std::string in der Funktion haben? Dann nimm doch einfach

    void foo(std::string meineigenerstring) {
        …
    }
    

    Ob das Ding kopiert oder gemoved wird, hängt davon ab, ob der Aufrufer std::thread bzw std::async einen Lvalue-string oder Rvalue-string übergibt. Und da wir wissen, dass std::string einen sehr effizienten Move-Konstruktor hat, können wir uns hier auch die Sache mit der Überladung sparen. Das einzige, was wir uns damit sparen würden, wäre nämlich ein Move, aber der ist ja billig bei std::string .

    Und auch wenn Du das hier schreibst:

    void foo(const std::string& constref) {
        …
    }
    

    musst Du dir keine Sorgen über Lebenszeiten machen, weil bei Aufrufen wie

    std::thread (foo, std::string("temp"));
    

    std::thread die Argumente immer in den anderen Thread kopiert/moved. Wenn Du einem neuen Thread explizit eine Referenz übergeben willst, ginge das so:

    void foo(std::string& nonconstref) {
        nonconstref = "blupp";
    }
    
    int main() {
       string blah;
       std::thread t (foo, std::ref(blah));
       t.join();
       std::cout << blah << std::endl;
    }
    

    Man muss hier also extra std::ref hinschreiben, damit der string nicht gemoved oder kopiert wird.

    Wenn Dir das nicht ganz geheuer ist, kannst du das natürlich auch per Lambda-Ausdruck machen. Dann klappt das mit der Überladung eh besser, falls Du sowas wirklich willst:

    void foo(std::string meins) {
        …
    }
    
    int main() {
       string blah = "hello world";
       std::thread t ( [blah = move(blah)] { foo(move(blah)); } ); // C++14
       t.join();
    }
    

    Der String zieht zunächst in das Lambda-Objekt um, welches dann in den neuen Thread umzieht und dann den string weiter an foo übergibt ohne irgendwo den string jemals zu kopieren. Aber das ist wirklich äquivalent zu

    void foo(std::string meins) {
        …
    }
    
    int main() {
       string blah = "hello world";
       std::thread t (foo, move(blah));
       t.join();
    }
    

    Der einzige Vorteil, den Dir hier ein Lambda-Ausdruck bietet, ist, dass Du damit besser klarkommst, wenn foo tatsächlich überladen wurde.

    HTH
    kk


Log in to reply