RValue Refs, std::move, best practice
-
Im Thread http://www.c-plusplus.net/forum/319529 kam folgendes Statement:
Nathan schrieb:
Deswegen ist es optimal wenn man jede Funktion mit einer const LValue Referenz und einer RValue Referenz überlädt. Der erste Fall ist gut für LValues, der zweite Fall gut für RValues, da dort direkt ins Ziel gemoved werden kann, was billiger als eine Kopie ist.
Da diese Überladungen bei N Argumenten zu 2^N Funktionen führen kann, nimmt man quasi als Kompromiss pass-by-Value. Bei LValues wird ins Argument kopiert und dann kann man vom Argument ins Ziel moven (explizit mit std::move), bei RValues wird ins Argument gemoved und man kann dan wie bei LValues ins Ziel moven.Klingt vernuenftig.
Deswegen ein Verstaendisfragen zu "best practice" anhand etwas aelteren Code von mir http://ideone.com/CImxeL bzgl. Nathans Anmerkungen. Es ist eine Messagequeue fuer multithreaded Producer/Consumer abgeleitet aus der Reihe http://www.youtube.com/playlist?list=PL1835A90FC78FF8BE . Explizit geht es um die Methode:
void post(T&& msg) { std::lock_guard<std::mutex> lock(mtx); queue.push_front(std::move(msg)); cond.notify_one(); }Grundgedanke war, dass keine Messages kopiert werden sollen.
Variante 1: Weicht man diese Anforderung auf, so waere es gut, zwei Methoden anzubieten:
void post(T const& msg) ... // copy messages void post(T&& msg) ... // move messagesWelche Methode wird bei temporaeren Objekten aufgerufen?
Variante 2: Da es wie schon gesagt fuer mehr Parameter recht aufwendig wird, sollte nur eine Methode angeboten werden:
void post(T msg)Und es wird dem Aufrufer ueberlassen, zu verschieben oder zu kopieren:
q.post(my_ms); // copy message q.post(std::move(my_msg)); // move messagePrinzipiell gefaellt mir der Gedanke, den Benutzer entscheiden zu lassen, ob kpiert werden soll
Meine Fragen: Ist Variante 2 "best practice"? Gibt es Nachteile? Gibt es weitere Vorteile?
-
Korrigiert mich wenn ich falsch liege, aber so wie ich das sehe, sind beide Varianten vom Aufrufverhalten gleich und auch das Verhalten ist praktisch identisch. Der Unterschied: Bei Variante 1 spart man sich einen zusätzlichen move, dafür muss man aber zwei Varianten implementieren.
-
Hi,
in einem Thread von mir gab es eine ganze Reihe von Antworten, die viele Details dazu geben, was man in welchem Fall optimal einsetzen sollte. Vielleicht hilft Dir das

http://www.c-plusplus.net/forum/314315
-
knivil schrieb:
Variante 1: Weicht man diese Anforderung auf, so waere es gut, zwei Methoden anzubieten:
void post(T const& msg) ... // copy messages void post(T&& msg) ... // move messagesWelche Methode wird bei temporaeren Objekten aufgerufen?
Bei Temporären Variablen wird natürlich die zweite Variante aufgerufen.
Variante 2: Da es wie schon gesagt fuer mehr Parameter recht aufwendig wird, sollte nur eine Methode angeboten werden:
void post(T msg)Und es wird dem Aufrufer ueberlassen, zu verschieben oder zu kopieren:
q.post(my_ms); // copy message q.post(std::move(my_msg)); // move messagePrinzipiell gefaellt mir der Gedanke, den Benutzer entscheiden zu lassen, ob kpiert werden soll
Meine Fragen: Ist Variante 2 "best practice"? Gibt es Nachteile? Gibt es weitere Vorteile?
imho ist Variante 2 best practice. Nachteil ist, dass die unter Umständen nicht ganz so effizient sein kann.
Kosten Variante 1:
LValue: 1 copy (Argument zum Ziel).
RValue: 1 move (Argument zum Ziel).
Kosten Variante 2:
LValue: 1 copy (Quelle zum Argument), 1 move (Argument zum Ziel)
RValue: 2 move (Temporary zum Argument, Argument zum Ziel)*(Wenn man natürlich aus dem LValue ein RValue macht, ist das effizienter.)
*Ideone überrascht mich da aber: http://ideone.com/GtERzZ
Kann es sein, dass der Compiler das Temporary direkt im Argument erstellt?
Wenn ja, ist diese Variante genauso gut, wie die mit T&&.
-
Best Practise waere, beides anzubieten. Die Standardcontainer bieten auch beides an. Das heisst aber nicht, dass man nicht beides ueber die selbe Funktion implementieren kann:
template <typename T> void post_impl(T&& msg) { std::lock_guard<std::mutex> lock(mtx); queue.push_front(std::forward<T>(msg)); cond.notify_one(); } void post(T const& msg) { post_impl(msg); } void post(T&& msg) { post_impl(std::move(msg)); }
-
Kellerautomat schrieb:
Best Practise waere, beides anzubieten. Die Standardcontainer bieten auch beides an.
Klar, in diesem Fall wäre es das, da es nur ein Argument gibt.
Hat man aber mehrere ist die Arbeit für den Programmierer doch etwas hoch.
Ich hab mal eine Klasse gesehen, die das Argument entweder als LValue oder RValue Referenz speichert, jenachdem was es ist. Das ermöglicht dann effiziente Übergabe an die entsprechenden Copy/Movectors.
-
Nathan schrieb:
*Ideone überrascht mich da aber: http://ideone.com/GtERzZ
Kann es sein, dass der Compiler das Temporary direkt im Argument erstellt?Wenn ich das richtig gelesen habe, ist es in der Tat so, dass Compiler nicht benötigte Kopien/Moves in Argumente komplett weg optimieren können, ähnlich wie beim Rückgabewert.
Nathan schrieb:
Wenn ja, ist diese Variante genauso gut, wie die mit T&&.
Fast, denn das weg optimieren geht wohl nur bei echten Temporaries und nicht bei std::move.
-
Kellerautomats Code mit richtigen Bezeichnern:
template <typename... T> void emplace(T&&... msg) { std::lock_guard<std::mutex> lock(mtx); queue.emplace_front(std::forward<T>(msg)...); cond.notify_one(); } void post(T const& msg) { emplace(msg); } void post(T&& msg) { emplace(std::move(msg)); }
-
Nathan schrieb:
Ich hab mal eine Klasse gesehen, die das Argument entweder als LValue oder RValue Referenz speichert, jenachdem was es ist. Das ermöglicht dann effiziente Übergabe an die entsprechenden Copy/Movectors.
Hier sowas in der Art meinte ich: http://ideone.com/fTORB3
Es kommen zwar zustätzliche Kosten (if Abfrage und ein weitere Def-Ctor), aber das ist in der Regel billig. Und man hat somit das beste von beiden Seiten: genau die Aufrufe, die man haben will, und wenig Tipparbeit.
-
Nathan schrieb:
Hier sowas in der Art meinte ich: http://ideone.com/fTORB3
Es kommen zwar zustätzliche Kosten (if Abfrage und ein weitere Def-Ctor), aber das ist in der Regel billig. Und man hat somit das beste von beiden Seiten: genau die Aufrufe, die man haben will, und wenig Tipparbeit.

-
Hier noch eine verbesserte Version, die auch mit Konvertierungen klar kommt und keinen zustätzlichen Overhead (abgesehen vom if-else und dem Erzeugen das fast_arg) bietet: http://ideone.com/uIIPMi
-
emplac0r schrieb:
Kellerautomats Code mit richtigen Bezeichnern:
template <typename... T> void emplace(T&&... msg) { std::lock_guard<std::mutex> lock(mtx); queue.emplace_front(std::forward<T>(msg)...); cond.notify_one(); } void post(T const& msg) { emplace(msg); } void post(T&& msg) { emplace(std::move(msg)); }
-
Warum nicht einfach so?
template <typename U> void post(U &&msg) { std::lock_guard<std::mutex> lock(mtx); queue.push_front(std::forward<U>(msg)); cond.notify_one(); }
-
TyRoXx schrieb:
Warum nicht einfach so?
Um es sich nicht mit Stroustrup zu verscherzen.
Stroustrup schrieb:
Improved push operations: My favorite container operation is push_back() that allows a container to grow gracefully:
vector<pair<string,int>> vp; string s; int i; while(cin>>s>>i) vp.push_back({s,i}); // {s,i} geht nur, wenn kein Template
-
Danke fuer eure Vorschlaege, std::forward werde ich wohl etwas naeher beleuchten. Aber eine Hilfsklasse wird es nicht geben. Tipparbeit ist fuer mich nie ein Grund.
-
knivil schrieb:
Danke fuer eure Vorschlaege, std::forward werde ich wohl etwas naeher beleuchten. Aber eine Hilfsklasse wird es nicht geben. Tipparbeit ist fuer mich nie ein Grund.
Es geht nicht nur um Tipparbeit, sondern auch um Übersichtlichkeit und Wartbarkeit. Bei doppeltem Code kann man zwei Fehler korrigieren müssen oder in einer Implementierung einen Fehler haben, den man schwer findet.