perfekt forwarding richtig verstehen
-
hallo,
ich habe perfect forwarding noch nicht richtig verstanden:
oder zumindest, wie man funktionen schreibt, die mit typen hantieren, die viel zu kopieren sind.Dazu habe ich mir ein kleines mwe überlegt, das keinen wirklichen sinn ergibt sondern nur eine wrapper funktion implementiert, mit der anschließend perfect forwarding gemacht werden kann.
vor c++11 wurden doch die funktionen so geschrieben, dass constrefs übergeben wurden, also etwa sowas:
#include <vector> #include <initializer_list> #include <iostream> class cls { public: cls() = delete; cls(int intval, std::initializer_list<int> vec) : _intval(intval), _vec(vec){ std::cout << "called Parametrized-Construct" << std::endl; } cls(const cls &inst) : _intval(inst._intval), _vec(inst._vec){ std::cout << "called Copy-Construct" << std::endl; } cls(cls &&inst) : _intval(std::move(inst._intval)), _vec(std::move(inst._vec)){ std::cout << "called Move-Construct" << std::endl; } ~cls() {std::cout << "called Destruct" << std::endl;} int _intval; std::vector<int> _vec; }; template <typename T> void func_old(const T &arg) { std::cout << "int:" << arg._intval << ", vec: "; for(int i : arg._vec) { std::cout << i << ", "; } std::cout << std::endl; } template <typename T> void wrapper_old(const T &arg) { func_old(arg); } int main() { { cls a(1, {1,2,3,4,5}); cls b(3, {4,5,3,4,5}); wrapper_old(a); wrapper_old(cls(2, {2,3,4,5,6})); wrapper_old(cls(a)); wrapper_old(std::move(a)); wrapper_old(cls(std::move(b))); } return 0; }
Mit C++11 wurde nun perfect forwarding eingeführt und es sind nun auch universal-references möglich:
Also etwa sowas:template <typename T> void func_new(T &&arg) { std::cout << "int:" << arg._intval << ", vec: "; for(int i : arg._vec) { std::cout << i << ", "; } std::cout << std::endl; } template <typename T> void wrapper_new(T &&arg) { func_new(std::forward<decltype(arg)>(arg)); } int main() { { cls a(1, {1,2,3,4,5}); cls b(3, {4,5,3,4,5}); wrapper_new(a); wrapper_new(cls(2, {2,3,4,5,6})); wrapper_new(cls(a)); wrapper_new(std::move(a)); wrapper_new(cls(std::move(b))); } return 0; }
Ist diese neue Form nun besser oder schlechter?
Wie sollte man nun funktionen schreiben? mit der alten methode, mit der neuen oder gibt es eine noch bessere alternative?danke
-
warewin schrieb:
Ist diese neue Form nun besser oder schlechter?
Wie sollte man nun funktionen schreiben? mit der alten methode, mit der neuen oder gibt es eine noch bessere alternative?Wenn die Referenz des Objekts nicht irgendwohin weitergereicht ("forwarded") wird, wo du von einem "move" des Objekts (im Gegensatz zum kopieren) profitierst, macht es in meinen Augen ein "perfect forwarding" im Allgemeinen keinen Sinn. Besonders, wenn du wie in deinem Beispiel lediglich Daten ausgibst.
std::forward
macht beispielsweise dann Sinn, wenn du innerhalb vonfunc_new
den Parameterarg
in einen Container einfügen würdest:template <typename T> void wrapper_new(T &&arg) { func_new(std::forward<T>(arg)); } template <typename T> void func_new(T &&arg) { std::vector<T> v; // Muss natürlich std::forward<T>(arg) statt // std::forward(arg) heissen, Dank an Arcoth // für den aufmerksamen Code-Review! v.push_back(std::forward<T>(arg)); } ... // Alle in den 4 Funktionsaufrufen übergebenen // Argumente sind rvalues (temporäre, namenlose // Objekte oder mit std::move explizit in eine // rvalue-Referenz gecastet). wrapper_new(cls(2, {2,3,4,5,6})); wrapper_new(cls(a)); wrapper_new(std::move(a)); wrapper_new(cls(std::move(b)));
Hier sorgen die
std::forward
dafür dass in allen 4 Fällen die "value"-Kategorie des ursprünglichen Arguments (rvalue) erhalten bleibt, und die Methodestd::vector<T>::push_back(T&&)
aufgerufen wird, die ihrerseits wieder dafür sorgt (wahrscheinlich ebenfalls durch einstd::forward
), dass die eingefügten Objekte mittels des Move-Konstruktors voncls
erzeugt werden.Oder vielleicht etwas simpler:
template <typename T> void func_new(T arg) { } template <typename T> void wrapper_new(T &&arg) { func_new(std::forward<T>(arg)); } ... wrapper_new(a); wrapper_new(cls(a)); wrapper_new(std::move(a));
... solte ausgeben:
called Copy-Construct called Move-Construct called Move-Construct
Wenn am Ende der Kette das "geforwardete" Objekt jedoch nicht "gemoved" wird, erschließt sich mir auf Anhieb kein Grund, weshalb man ein solches "Forwarding" einsetzten sollte (auch wenn es sicher gute, jedoch eher exotische Gründe geben mag).
Um deine Frage also konkret zu beantworten: So wie die Funktionen bei dir definiert sind, sind
func_old
undwrapper_old
völlig ausreichend und perfect forwarding bringt dir keinen Vorteil.Gruss,
FinneganP.S.: Habe das
std::forward<decltype(arg)>
inwrapper_new()
durch einstd::forward<U>
ersetzt. Letzteres ist auf jeden Fall nicht falsch, während ich mir bei ersterem unsicher bin, ob es äquivalent ist oder nicht eventuell zu einemstd::forward<U&&>
oder einemstd::forward<U&>
expandiert wird, was soweit ich informiert bin nicht korrekt ist.
-
Ja, du solltest auf jeden Fall forwarden. Schon allein wegen der Möglichkeit dass die Funktion die am Ende einer forwarding-Kette steht, e.g.
func_new
, künftig in einer Weise modifiziert wird die forwarding benötigt/von forwarding profitiert.
-
P.S.: Habe das
std::forward<decltype(arg)>
inwrapper_new()
durch ein std::forward<U> ersetzt. Letzteres ist auf jeden Fall nicht falsch, während ich mir bei ersterem unsicher bin, ob es äquivalent ist oder nicht eventuell zu einemstd::forward<U&&>
oder einemstd::forward<U&>
expandiert wird, was soweit ich informiert bin nicht korrekt ist.Es ist äquivalent. Dabei muss vor allem reference collapsing sowohl im Rückgabetyp von
forward
als auch im Parameter vonwrapper_new
berücksichtigt werden.template< class T > T&& forward( typename std::remove_reference<T>::type& t ); template< class T > T&& forward( typename std::remove_reference<T>::type&& t ); template <typename U> void wrapper_new(U &&arg) { forward<decltype(arg)>(arg) }
Wenn
U
eine Referenz ist, istdecltype(arg) == U
(aufgrund von reference collapsing).
WennU
keine Referenz ist, istdecltype(arg) == U&&
. Allerdings gibt es dann durchT&&
inforward
wiederum reference collapsing, sodass der RückgabetypU&&
wird, was er auch sein soll (da das Argument ein rvalue war und als solches weitergeleitet werden soll).Allerdings ist
v.push_back(std::forward(arg));
Schwachsinn und sollte nicht kompilieren.
-
Arcoth schrieb:
Ja, du solltest auf jeden Fall forwarden. Schon allein wegen der Möglichkeit dass die Funktion die am Ende einer forwarding-Kette steht, e.g.
func_new
, künftig in einer Weise modifiziert wird die forwarding benötigt/von forwarding profitiert.Ich glaube es schadet der Lesbar- und Wartbarkeit des Codes ganz ordentlich, wenn man beginnen würde, aus jeder Funktion bei der die Argumente irgendwann mal irgendwohin gemoved werden können eine Template-Funktion zu machen, bei der alle betreffenden Parameter-Typen deduzierbar sind.
Bei Funktionen wo es deutlich absehbar ist, dass man profitiert, gebe ich dir recht, bei allen anderen möchte ich jedoch nicht erst das "root of all evil" bemühen müssen
Was natürlich stimmt ist, dass wenn ich schon ein
template <typename T> void wrapper_new(T &&arg)
habe, ein
std::forward
so ziemlich die einzig sinnvolle Wahl ist: Einstd::move
kann ich hier nicht machen, da die Funktion auch lvalues "frisst", und wenn ich nicht ausnutze, dassarg
auch eine rvalue-Referenz sein kann, kann ich mir das&&
-Geraffel auch gleich sparenFinnegan
-
Ich glaube es schadet der Lesbar- und Wartbarkeit des Codes ganz ordentlich, wenn man beginnen würde, aus jeder Funktion bei der die Argumente irgendwann mal irgendwohin gemoved werden können eine Template-Funktion zu machen, bei der alle betreffenden Parameter-Typen deduzierbar sind.
Wenn ich Argumente an etwas forwarde was diese momentan kopiert (nicht lediglich auf die Daten zugreift o.ä.), sollte man schon forwarden. Den Kopien sollten an allen Ecken vermieden werden, darum geht es bei perfect forwarding. (Und um overload resolution.)
-
Arcoth schrieb:
Wenn ich Argumente an etwas forwarde was diese momentan kopiert (nicht lediglich auf die Daten zugreift o.ä.), sollte man schon forwarden. Den Kopien sollten an allen Ecken vermieden werden, darum geht es bei perfect forwarding. (Und um overload resolution.)
Nichts anderes habe ich behauptet, allerdings war die Frage ob die
wrapper/func
_new
oder_old
-Funktionen besser/schlechter sind, und für den gegebenen Code sind die_new
-Varianten schlechter weil sie dasselbe Resultat mit umständlicherem Code erzielen.Arcoth schrieb:
Allerdings ist
v.push_back(std::forward(arg));
Schwachsinn und sollte nicht kompilieren.
Danke für den diskteten Hinweis auf den Tippfehler, ich wusste gar nicht, dass du auf die Diplomatenschule gegangen bist
Finngean