CPP2019SSF10: Unterschied zwischen den Versionen

Aus Verteilte Systeme - Wiki
Wechseln zu: Navigation, Suche
(Syntax)
(Warnhinweise)
 
(44 dazwischenliegende Versionen desselben Benutzers werden nicht angezeigt)
Zeile 12: Zeile 12:
  
 
=== Use Cases ===
 
=== Use Cases ===
 +
Rvalue reference ist ein Mechanismus um
 
# move semantics
 
# move semantics
 
# perfect forwarding
 
# perfect forwarding
 +
zu ermöglichen.
 +
 +
 +
Um zu verstehen was eine rvalue reference betrachten wir zunächst den unterschied zwischen rvalues und lvalues.
 +
 +
Rvalue: &3 // von einem rvalue kann man keine Adresse nehmen.
 +
 +
Lvalue: &int i // von einem lvalue kann kann man die Adresse erfragen.
 +
 +
Um std::move und std::forward zu verstehen kann es hilfreich sein zunächst einmal zu wissen was std::move und std::forward nicht tut. Std::move bewegt nichts, sondern castet und std::forward forwarded nichts, sondern castet nur unter bestimmten Bedingungen. Std::move und std::forward sind fuction templates.
 +
  
 
'''1) move semantics:'''
 
'''1) move semantics:'''
 +
 +
Std::move castet die übergebenen Argumente bedingungslos zu Rvalues.
 +
 +
Dies ist eine beispielhafte Implementierung von std::move.
 +
<source lang="cpp">
 +
template<typename T>
 +
typename remove_reference<T>::type&&  move(T&& param)
 +
{
 +
  using ReturnType = typename remove_reference<T>::type&&;
 +
 
 +
  return static_cast<ReturnType>(param);
 +
}
 +
</source>
 +
 +
Die Funktion nimmt eine Reference auf ein Objekt und gibt eine Referenz auf das selbe Objekt zurück.
 +
T&& impliziert das die Funktion eine Rvalue Referenz zurück gibt. Würde man jedoch einen lvalue übergeben, würde T&& nach den Reference Collapsing Rules zu T& kollabieren und wäre damit ein Lvalue Referenz. Um dies zu vermeiden wird die übergebe Referenz zunächst mit remove_reference<T> entfernt und anschließend zu einer Rvalue Referenz gecastet. Damit ist sicher gestellt, das std::move seine arugmente '''immer zu einer Rvalue Referenz castet'''. Das ist wichtig, da Rvalue Referenzen die von Funktionen zurückgegebene werden Rvalues sind.
 +
 +
 +
Beispiel zur Erklärung von move semantics (std::move):
 +
 +
Ziel bei der Verwendung von std::move ist es teuere Kopier Operationen zu vermeiden.
 +
Der übergebene string text soll also an value weitergereicht werden:
 +
<source lang="cpp">
 +
#include <iostream>
 +
#include <string>
 +
using namespace std;
 +
 +
struct Annotation {
 +
    public:
 +
    explicit Annotation(const std::string text) : value(std::move(text)) // "move" text into value
 +
    {
 +
        cout << "text = " << text << endl;
 +
        cout << "value = " << value << endl;
 +
    }
 +
    private:
 +
    std::string value;
 +
};
 +
 +
int main()
 +
{
 +
    Annotation("Hallo");
 +
    return 0;
 +
}
 +
</source>
 +
 +
Achtung! Dieser Code tut nicht wonach es aussieht, da text nicht in value verschoben, sondern kopiert wird!
 +
Im Beispiel wird text durch std:: move zu einem rvalue gecastet. Jedoch ist text ein const std::string!
 +
Das bedeutet also das text vor dem cast ein lvalue const std::string ist und ''danach ein rvalue const std::string'',
 +
die constness ist also troz cast nicht verschwunden!
 +
 +
 +
Um zu verstehen, warum nicht der move Konstruktor, sondern der copy Konstruktor aufgerufen wird betrachten wir die  Klasse std::string, welche einen move und einen copy Konstruktor enthält.
 +
 +
<source lang="cpp">
 +
class string {
 +
    public:
 +
    ...
 +
    string(const string& rhs);  // copy ctor
 +
    string(string&& rhs);        // move ctor
 +
    ...
 +
};
 +
</source>
 +
 +
In der initializer list der struct Annotation ist das Ergebnis von std::move(text) ein rvalue vom Typ const std::string.
 +
Dieser rvalue kann ''nicht'' an den move Konstruktor der std::string Klasse übergeben werden, da der move Konstruktor eine rvalue reference auf eine '''non'''-const std::string  nimmt.
 +
Jedoch kann der rvalue an den copy Konstruktor übergeben werden, da eine lvalue-reference-to-const an einen const rvalue gebunden werden kann.
 +
Obwohl also text zu einem rvalue gecastet wurde wird bei der member intitialization der copy Konstruktor aufgerufen!
 +
 +
Was lernen wir daraus? Erstens: Wenn man Objekte moven können will, darf man sie nicht const deklarieren.
 +
Zweitens: std::move garantiert nicht, das das Objekt, welches gecastet wird überhaupt `gemoved` werden darf.
 +
Das Einzige wovon wir uns sicher sein können, ist das std::move ein rvalue zurückgibt.
 +
 +
 +
'''Ein optionaler Erklärungsversuch, warum mit move semantics teure kopier Operationen vermieden werden können:'''
 +
 +
Im Beispiel der struct Annotation wurde erwähnt, das das Ziel von move Semantics sei, teure kopier Operationen zu vermeiden.
 
Move semantics ermöglichen es Resourcen von einem Objekt zu einem anderen zu transferieren.
 
Move semantics ermöglichen es Resourcen von einem Objekt zu einem anderen zu transferieren.
Das ist möglich, da man sich sicher sein kann, das ein rvalue nirgendwo sonst im Programm referenziert wird. Zur Implementierung fügt man der Klasse üblichere weiße einen move constructor (und optional einen move assignment operator (operator=) ) hinzu. Alle copy oder assignment Operationen nutzen dann automatisch den move- bzw move assignment operator.
+
Das ist möglich, da man sich sicher sein kann, das ein ''rvalue nirgendwo sonst im Programm referenziert wird''. Zur Implementierung fügt man der Klasse üblichere weiße einen move constructor (und optional einen move assignment operator (operator=) ) hinzu. Alle copy oder assignment Operationen nutzen dann automatisch den move- bzw move assignment operator.
 +
 
 +
Ein Beispiel hierfür sei das einfügen von Elementen in einen Vektor:
 +
 
 +
Wenn ein Vektor vollständig gefüllt ist und ein weiteres Element eingefügt werden soll, muss das Vektor Objekt erst neuen Speicherplatz für seine Elemente allozieren um anschließend alle Elemente an den neuen Speicherplatz zu kopieren. Beim Kopieren wird erst mittels dem ''default-Konstruktor'' ein neues Element angelegt, dann der ''copy-Konstruktor'' aufgerufen, um die Daten des alten Elements in das neue Element zu kopieren und anschließend das alte Element mittels des ''Destruktors'' zerstört.
  
Besonders deutlich wird der Vorteil der move semantic, wenn man sich überlegt wie Elemente in einen Vektor eingefügt werden. Wenn ein Vektor vollständig gefüllt ist und ein weiteres Element eingefügt werden soll, muss das Vektor Objekt erst neuen Speicherplatz für seine Elemente allozieren um anschließend alle Elemente an den neuen Speicherplatz zu kopieren. Beim Kopieren wird erst mittels dem default-Konstruktor ein neues Element angelegt, dann der copy-Konstruktor aufgerufen, um die Daten des alten Elements in das neue Element zu kopieren und anschließend das alte Element mittels des Destruktors zerstört.
+
'''Beim Aufruf des copy-Konstruktors wird erneut Speicher alloziert.'''
Beim Aufruf des copy-Konstruktors wird erneut Speicher alloziert.
 
 
Dieser zeit- und speicheraufwendige Prozess kann durch einen move-Konstruktor stark vereinfacht werden.
 
Dieser zeit- und speicheraufwendige Prozess kann durch einen move-Konstruktor stark vereinfacht werden.
 +
 
Bsp.: für move Konstruktor:
 
Bsp.: für move Konstruktor:
 +
 
<source lang="cpp">
 
<source lang="cpp">
 
/* move Konstruktor:
 
/* move Konstruktor:
Zeile 41: Zeile 134:
 
}
 
}
 
</source>
 
</source>
 +
 +
  
 
'''2.) perfect forwarding:'''
 
'''2.) perfect forwarding:'''
 +
 
Unter perfect forwarding versteht man die Möglichkeit, Template-Funktionen zu schreiben, die ihre Argumente unverändert an andere Funktionen weiterreichen können.
 
Unter perfect forwarding versteht man die Möglichkeit, Template-Funktionen zu schreiben, die ihre Argumente unverändert an andere Funktionen weiterreichen können.
Hiermit wird das forwarding problem umgangen, das auftritt wenn man eine generische Funktion schreibt, die Referenzen als Parameter annimmt.
+
Auch std::forward castet immer zu einem Rvalue, jedoch nur unter gewissen Bedingungen.
 +
 
 +
<source lang="cpp">
 +
#include <iostream>
 +
#include <stdio.h>
 +
using namespace std;
 +
 
 +
// Funktion die lvalues nimmt.
 +
void process(const int& lvalArg){ cout << "lvalue" << endl; };
 +
 
 +
// Funktion die rvalues nimmt.
 +
void process(int&& rvalArg){ cout << "rvalue" << endl; };
 +
 
 +
 
 +
// Template, das Argumente unverändert an Funktionen weiter gibt.
 +
template<typename T>
 +
void logAndProcess(T&& param)
 +
{
 +
    // Here you could log the time, or whatever...
 +
    process(std::forward<T>(param));    // falls logAndProcess() mit einem rvalue aufgerufen wurde,
 +
                                        // caste param zu einem rvalue!
 +
}
 +
 
 +
 
 +
int main() {
 +
    int i;
 +
        logAndProcess(i);              // Aufruf mit lvalue
 +
        logAndProcess(std::move(i));    // Aufruf mit rvalue
 +
}
 +
</source>
 +
 
 +
In der logAndProcess Funktion wird der Parameter param an die Funktion process übergeben.
 +
Wenn man nun einen lvalue übergibt erwartet man natürlich, das auch die überladene `process` Funktion für lvalues aufgerufen wird, beziehungsweise die rvalue `process` Fuktion, wenn man einen rvalue übergibt.
 +
Jedoch ist param – genau wie alle Funktionsparameter – ein lvalue.
 +
Damit nun nicht mit jedem Aufruf von logAndProcess die `process` Funktion für lvalues aufgerufen wird brauchen wir einen Mechanismus für param, der param zu einem rvalue castet, genau dann wenn logAndProcess mit einem rvalue aufgerufen wurde.
 +
 
 +
Woher weiß nun forward, das es mit einem rvalue aufgerufen wurde?
 +
Nach den Reference Collapsing Rules von C++11 zerfallen mehrere Referenz zu einer einzigen:
 +
  1. T&  &  ==>  T&
 +
  2. T&  &&  ==>  T&
 +
  3. <span style="color: red">T&&</span style> <span style="color: orange">&</span style>  ==>  T&
 +
  4. <span style="color: red">T&&</span style> <span style="color: green">&&</span style>  ==>  T&&
 +
 
 +
Betrachten wir nun unsere Funktion:
 +
 
 +
void logAndProcess(<span style="color: red">T&&</span style> <span style="color: green">param</span style>)
 +
 
 +
Ruft man die Template Funktion nun mit einem rvalue auf, also zB. ein interger Wert → logAndProcess(std::move(i));
 +
wird der Typ <T> durch int ersetzt. Dadurch ist der Typ nun <span style="color: red">int&&</span style>.
 +
Da das Argument ein Rvalue (<span style="color: green">&&</span style>) ist, ist der Komplette Ausdruck in dem runden Klammern:
 +
int&& &&. Durch die reference collapsing rules wird dieser Ausdruck zu int&& reduziert
 +
und eine Rvalue referenz wird an die foo() Funktion übergeben.
 +
 
 +
Ruft man die Template Funktion hingegen mit einem lvalue auf, so wird nach den reference collapsing rules
 +
der Typ <T> wider durch eine Referenz ersetzt: z.B. int i; →  logAndProcess(i) → <span style="color: red">int&&</span style>.
 +
Da das Argument ein Lvalue (<span style="color: orange">&</span style>) ist, ist der Komplette Ausdruck in den runden Klammern:
 +
int&& &, welcher zu einer lvalue reference (int&) zerfällt.
 +
 
 +
  Wenn param mit einer rvalue reference intitialisiert wird, ist T&& eine rvalue reference.
 +
  logAndProcess(i); =>  <T> is replaced by int&& =>  (T&& param) = int&& && param= int&& param
 +
  Wenn param mit einer lvalue reference intitialisiert wird, ist T&& eine lvalue reference.
 +
  logAndProcess(std::move(i)); =>  <T> is repaced by int&  =>  (T&& param) = int&&  & param= int& param
 +
 
 +
Mit perfect forwarding wird also das forwarding Problem umgangen, das auftritt wenn man eine generische Funktion schreibt, die Referenzen als Parameter annimmt.
 +
 
 +
 
 +
Die folgenden Beispiele sollen noch einmal verdeutlichen, das ''nur'' mit verwendung der std::forwad Funktion ''alle Argumente'' unverändert an eine Zielfunktion (im Beispiel die Funktion foo() ) weitergereicht werden können.
 
Nimmt zum Beispiel eine generische Funktion als Parameter eine const T& („const Referenz“), dann kann die aufgerufene Funktion den Wert des Parameters nicht verändern.
 
Nimmt zum Beispiel eine generische Funktion als Parameter eine const T& („const Referenz“), dann kann die aufgerufene Funktion den Wert des Parameters nicht verändern.
  
 
<source lang="cpp">
 
<source lang="cpp">
 
template< typename T >
 
template< typename T >
void relay(const T& arg) {             // argument forwarding to foo()
+
void relay(const T& arg) {         // argument forwarding to foo()
     foo( arg );                                   // arg can not be changed!
+
     foo( arg );                   // arg can not be changed!
 
}
 
}
  
 
int main() {
 
int main() {
 
     int reusable = 4;
 
     int reusable = 4;
     relay(reusable);   // called with lvalue ->  Works fine!
+
     relay(reusable);               // called with lvalue ->  Works fine!
     Relay(4);             // called with rvalue ->  Does not work!
+
     Relay(4);                     // called with rvalue ->  Does not work!
 
}
 
}
 
</source>
 
</source>
  
Nimmt die Funktion eine T& („Referenz“), kann sie nicht über einen Rvalue (T&&) aufgerufen werden.
+
Auch wenn die Funktion eine nicht const T& („Referenz“) nimmt, kann sie nicht über einen Rvalue (T&&) aufgerufen werden.
  
 
<source lang="cpp">
 
<source lang="cpp">
 
template< typename T >
 
template< typename T >
void relay(T& arg) {                       // arg. forwarding to foo()
+
void relay(T& arg) {               // arg. forwarding to foo()
 
     foo( arg );
 
     foo( arg );
 
}
 
}
Zeile 70: Zeile 232:
 
int main() {
 
int main() {
 
     int reusable = 4;
 
     int reusable = 4;
     relay(reusable);   // called with lvalue ->  Works fine!
+
     relay(reusable);               // called with lvalue ->  Works fine!
     Relay(4);             // called with rvalue ->  Does not work!
+
     Relay(4);                       // called with rvalue ->  Does not work!
 
}
 
}
 
</source>
 
</source>
Zeile 80: Zeile 242:
 
<source lang="cpp">
 
<source lang="cpp">
 
template< typename T >
 
template< typename T >
void relay(T&& arg) {                     // arg. forwarding to foo()
+
void relay(T&& arg) {               // arg. forwarding to foo()
     foo( std::forward<T>(arg) );       // forward() casts arg to type of T&&.
+
     foo( std::forward<T>(arg) );   // forward() casts arg to type of T&&.
 
}
 
}
  
 
int main() {
 
int main() {
 
     int reusable = 4;
 
     int reusable = 4;
     relay(reusable);   // called with lvalue -> int copy ctor will be invoked
+
     relay(reusable);               // called with lvalue -> int copy ctor will be invoked
     relay(createInt()); // called with rvalue -> int move ctor will be invoked
+
     relay(createInt());             // called with rvalue -> int move ctor will be invoked
 
}
 
}
 
</source>
 
</source>
  
Nach den Reference Collapsing Rules von C++11 zerfallen mehrere Referenz zu einer einzigen:
+
=== Motivation für die Einführung ===
  1. T&  &  ==>  T&
+
Dank der Einführung von rvalue reference && ist eine Unterscheidung zwischen rvalues und lvalues möglich.
  2. T&  &&  ==>  T&
+
Von rvalues kann man keine Adresse bekommen, von lvalues schon.
  3. T&& &  ==>  T&
 
  4. T&& &&  ==>  T&&
 
  
void relay(T&& arg)
+
Bsp:
  
Ruft man die Template Funktion nun mit einem rvalue auf, also zB. ein interger Wert → relay(5);
+
&int i; // gibt Adresse von lvalue i zurück.
wird der Typ <T> durch int ersetzt. Dadurch ist der Typ nun int&&.
 
Da das Argument ein Rvalue (&&) ist, ist der Komplette Ausdruck in dem runden Klammern:
 
int&& &&. Durch die reference collapsing rules wird dieser Ausdruck zu int&& reduziert
 
und eine Rvalue referenz wird an die foo() Funktion übergeben.
 
  
Ruft man die Template Funktion hingegen mit einem lvalue auf, so wird nach den reference collapsing rules
 
der Typ <T> wider durch eine Referenz ersetzt: z.B. int x; → relay(x) → int&&.
 
Da das Argument ein Lvalue (&) ist, ist der Komplette Ausdruck in den runden Klammern:
 
int&& &, welcher zu einer lvalue reference (int&) zerfällt.
 
 
 
  Wenn arg mit einer rvalue reference intitialisiert wird, ist T&& eine rvalue reference.
 
  relay(5); =>  <T> is replaced by int&& =>  (T&& arg) = int&& && arg= int&& arg
 
  Wenn arg mit einer lvalue reference intitialisiert wird, ist T&& eine lvalue reference.
 
  relay(x); =>  <T> is repaced by int&  =>  (T&& arg) = int&&  & arg= int& arg
 
 
=== Motivation für die Einführung ===
 
Dank der Einführung von rvalue reference && ist eine Unterscheidung zwischen rvalues und lvalues möglich.
 
Von rvalues kann man keine Adresse bekommen, von lvalues schon. Bsp:
 
&int i; // gibt Adresse von lvalue i zurück.
 
 
&3;    // kann keine Adresse von rvalue 3 zurück geben.
 
&3;    // kann keine Adresse von rvalue 3 zurück geben.
  
Zeile 127: Zeile 268:
 
<source lang="cpp">
 
<source lang="cpp">
 
template< typename T >
 
template< typename T >
void relay(T& arg) {                           // für nicht const lvalue
+
void relay(T& arg) {               // für nicht const lvalue
 
     foo( arg );
 
     foo( arg );
 
}
 
}
 
template< typename T >
 
template< typename T >
void relay(T&& arg) {                       // für nicht const rvalue
+
void relay(T&& arg) {               // für nicht const rvalue
 
     foo( arg );
 
     foo( arg );
 
}
 
}
 
template< typename T >
 
template< typename T >
void relay(const T& arg) {                 // für const lvalue
+
void relay(const T& arg) {         // für const lvalue
 
     foo( arg );
 
     foo( arg );
 
}
 
}
 
template< typename T >
 
template< typename T >
void relay(const T&& arg) {             // für const rvalue
+
void relay(const T&& arg) {         // für const rvalue
 
     foo( arg );
 
     foo( arg );
 
}
 
}
Zeile 145: Zeile 286:
  
 
=== Warnhinweise ===
 
=== Warnhinweise ===
Hinweis zu perfect forwarding:
+
'''Hinweis zu move semantics:'''
 +
 
 +
Wie man im ersten Beispiel zu move semantics sehen konnte, kann es passieren das man nicht den Effekt Erzielt den man intuitiv zu erzielen glaubt.
 +
Im Beispiel wurde die Variable text nicht in die variable value `gemoved`, sondern es wurde der copy Konstruktor der Klasse string aufgerufen.
 +
Um tatsächlich kopier Operationen zu vermeiden ist also eine Grundlegende Kenntnis der move semantics und von perfect forwarding notwendig.
 +
 
 +
'''Hinweis zu perfect forwarding:'''
 +
 
 
Guckt man sich die Implementierung von std::forward<T>(arg) an, so sieht man diese ein übergebenes Argument zu einer T&& reference castet, also zu genau dem Typ <T> der an die template Funktion übergeben wurde (egal ob rvalue, lvalue, const oder non-const).
 
Guckt man sich die Implementierung von std::forward<T>(arg) an, so sieht man diese ein übergebenes Argument zu einer T&& reference castet, also zu genau dem Typ <T> der an die template Funktion übergeben wurde (egal ob rvalue, lvalue, const oder non-const).
Hier kann keine std::move<T>(arg) funktion verwendet werden, da diese das übergebene Argument immer zu einem rvalue castet.
+
Diesen Effekt erreicht man nicht wenn man das std::move<T>(arg) Template verwendet, da diese das übergebene Argument '''immer''' zu einem rvalue castet.
Implementierung der std::forward() Funktion in utility.hpp :
+
 
 +
Implementierung des std::forward() Templates in utility.hpp :
 
<source lang="cpp">
 
<source lang="cpp">
  // forward/move:
 
 
   template <class T>
 
   template <class T>
 
   constexpr T&& forward(remove_reference_t<T>& t) noexcept;
 
   constexpr T&& forward(remove_reference_t<T>& t) noexcept;
Zeile 165: Zeile 313:
 
=== Code-Beispiele ===
 
=== Code-Beispiele ===
  
<source lang="cpp">
+
Beispiel für die Verwendung eines '''move-Konstruktors''':
#include <iostream>
 
#include <string>
 
using namespace std;
 
 
 
int main()
 
{
 
  string s = string("h") + "e" + "ll" + "o";
 
  cout << s << endl;
 
}
 
</source>
 
 
 
Beispiel für die Verwendung eines move-Konstruktors:
 
 
<source lang="cpp">
 
<source lang="cpp">
 
#include <iostream>
 
#include <iostream>
Zeile 213: Zeile 349:
 
int main()
 
int main()
 
{
 
{
  // string s = string("h") + "e" + "ll" + "o";
 
  // cout << s << endl;
 
 
 
   vector<A> v1;
 
   vector<A> v1;
 
   v1.push_back(A());
 
   v1.push_back(A());
  // v1.push_back(A());
 
  // v1.push_back(A());
 
 
  
 
   return 0;
 
   return 0;
Zeile 227: Zeile 357:
  
  
Beispiel für perfect forwading:
+
Beispiel für '''perfect forwading''':
 
<source lang="cpp">
 
<source lang="cpp">
 
#include <iostream>
 
#include <iostream>
Zeile 273: Zeile 403:
 
}
 
}
 
</source>
 
</source>
 +
 +
 +
'''Quellenangabe:'''
 +
 +
https://docs.microsoft.com/en-us/cpp/cpp/rvalue-reference-declarator-amp-amp?view=vs-2019
 +
 +
https://www.nosid.org/cxx11-perfect-forwarding.html
 +
 +
https://www.youtube.com/watch?v=IOkgBrXCtfo
 +
 +
https://www.youtube.com/watch?v=0xcCNnWEMgs
 +
 +
Buch: Effective Modern C++ von Scott Meyers (Seite 157 und Folgende)

Aktuelle Version vom 24. August 2019, 12:56 Uhr

Feature: Beispiel-Feature

  • Eingeführt in: C++11
  • Deprecated seit: --
  • Nicht mehr im Standard seit: immer noch im Standard

Syntax

  • type-id && cast-expression

Use Cases

Rvalue reference ist ein Mechanismus um

  1. move semantics
  2. perfect forwarding

zu ermöglichen.


Um zu verstehen was eine rvalue reference betrachten wir zunächst den unterschied zwischen rvalues und lvalues.

Rvalue: &3 // von einem rvalue kann man keine Adresse nehmen.

Lvalue: &int i // von einem lvalue kann kann man die Adresse erfragen.

Um std::move und std::forward zu verstehen kann es hilfreich sein zunächst einmal zu wissen was std::move und std::forward nicht tut. Std::move bewegt nichts, sondern castet und std::forward forwarded nichts, sondern castet nur unter bestimmten Bedingungen. Std::move und std::forward sind fuction templates.


1) move semantics:

Std::move castet die übergebenen Argumente bedingungslos zu Rvalues.

Dies ist eine beispielhafte Implementierung von std::move.

template<typename T>
typename remove_reference<T>::type&&  move(T&& param)
{
   using ReturnType = typename remove_reference<T>::type&&;
   
   return static_cast<ReturnType>(param);
}

Die Funktion nimmt eine Reference auf ein Objekt und gibt eine Referenz auf das selbe Objekt zurück. T&& impliziert das die Funktion eine Rvalue Referenz zurück gibt. Würde man jedoch einen lvalue übergeben, würde T&& nach den Reference Collapsing Rules zu T& kollabieren und wäre damit ein Lvalue Referenz. Um dies zu vermeiden wird die übergebe Referenz zunächst mit remove_reference<T> entfernt und anschließend zu einer Rvalue Referenz gecastet. Damit ist sicher gestellt, das std::move seine arugmente immer zu einer Rvalue Referenz castet. Das ist wichtig, da Rvalue Referenzen die von Funktionen zurückgegebene werden Rvalues sind.


Beispiel zur Erklärung von move semantics (std::move):

Ziel bei der Verwendung von std::move ist es teuere Kopier Operationen zu vermeiden. Der übergebene string text soll also an value weitergereicht werden:

#include <iostream>
#include <string>
using namespace std;

struct Annotation {
    public:
    explicit Annotation(const std::string text) : value(std::move(text)) // "move" text into value
    {
        cout << "text = " << text << endl;
        cout << "value = " << value << endl;
    }
    private:
    std::string value;
};

int main()
{
    Annotation("Hallo");
    return 0;
}

Achtung! Dieser Code tut nicht wonach es aussieht, da text nicht in value verschoben, sondern kopiert wird! Im Beispiel wird text durch std:: move zu einem rvalue gecastet. Jedoch ist text ein const std::string! Das bedeutet also das text vor dem cast ein lvalue const std::string ist und danach ein rvalue const std::string, die constness ist also troz cast nicht verschwunden!


Um zu verstehen, warum nicht der move Konstruktor, sondern der copy Konstruktor aufgerufen wird betrachten wir die Klasse std::string, welche einen move und einen copy Konstruktor enthält.

class string {
    public:
    ...
    string(const string& rhs);   // copy ctor
    string(string&& rhs);        // move ctor
    ...
};

In der initializer list der struct Annotation ist das Ergebnis von std::move(text) ein rvalue vom Typ const std::string. Dieser rvalue kann nicht an den move Konstruktor der std::string Klasse übergeben werden, da der move Konstruktor eine rvalue reference auf eine non-const std::string nimmt. Jedoch kann der rvalue an den copy Konstruktor übergeben werden, da eine lvalue-reference-to-const an einen const rvalue gebunden werden kann. Obwohl also text zu einem rvalue gecastet wurde wird bei der member intitialization der copy Konstruktor aufgerufen!

Was lernen wir daraus? Erstens: Wenn man Objekte moven können will, darf man sie nicht const deklarieren. Zweitens: std::move garantiert nicht, das das Objekt, welches gecastet wird überhaupt `gemoved` werden darf. Das Einzige wovon wir uns sicher sein können, ist das std::move ein rvalue zurückgibt.


Ein optionaler Erklärungsversuch, warum mit move semantics teure kopier Operationen vermieden werden können:

Im Beispiel der struct Annotation wurde erwähnt, das das Ziel von move Semantics sei, teure kopier Operationen zu vermeiden. Move semantics ermöglichen es Resourcen von einem Objekt zu einem anderen zu transferieren. Das ist möglich, da man sich sicher sein kann, das ein rvalue nirgendwo sonst im Programm referenziert wird. Zur Implementierung fügt man der Klasse üblichere weiße einen move constructor (und optional einen move assignment operator (operator=) ) hinzu. Alle copy oder assignment Operationen nutzen dann automatisch den move- bzw move assignment operator.

Ein Beispiel hierfür sei das einfügen von Elementen in einen Vektor:

Wenn ein Vektor vollständig gefüllt ist und ein weiteres Element eingefügt werden soll, muss das Vektor Objekt erst neuen Speicherplatz für seine Elemente allozieren um anschließend alle Elemente an den neuen Speicherplatz zu kopieren. Beim Kopieren wird erst mittels dem default-Konstruktor ein neues Element angelegt, dann der copy-Konstruktor aufgerufen, um die Daten des alten Elements in das neue Element zu kopieren und anschließend das alte Element mittels des Destruktors zerstört.

Beim Aufruf des copy-Konstruktors wird erneut Speicher alloziert. Dieser zeit- und speicheraufwendige Prozess kann durch einen move-Konstruktor stark vereinfacht werden.

Bsp.: für move Konstruktor:

/* move Konstruktor:
   Der move Konstruktor nimmt eine rvalue Reference &&
   und initialisiert die privaten Klassenvariablen zunächst mit
   Null bzw 0 */
MemoryBlock(MemoryBlock&& other) : _data(nullptr), _length(0)
{
    // hier übergibt der move Konstruktor die Daten und die Länge
    // an die Variablen _data und _length seiner Klasse:
    _data = other._data;
    _length = other._length;
    // und setzt anschließend _data und _length des übergebenen
    // Objektes auf 0 um zu verhindern, das der destruktor
    // mehrfach Speicher frei geben will.
    other._data = nullptr;
    other._length = 0;
}


2.) perfect forwarding:

Unter perfect forwarding versteht man die Möglichkeit, Template-Funktionen zu schreiben, die ihre Argumente unverändert an andere Funktionen weiterreichen können. Auch std::forward castet immer zu einem Rvalue, jedoch nur unter gewissen Bedingungen.

#include <iostream>
#include <stdio.h>
using namespace std;

// Funktion die lvalues nimmt.
void process(const int& lvalArg){ cout << "lvalue" << endl; };

// Funktion die rvalues nimmt.
void process(int&& rvalArg){ cout << "rvalue" << endl; };


// Template, das Argumente unverändert an Funktionen weiter gibt.
template<typename T>
void logAndProcess(T&& param)
{
    // Here you could log the time, or whatever...
    process(std::forward<T>(param));    // falls logAndProcess() mit einem rvalue aufgerufen wurde,
                                        // caste param zu einem rvalue!
}


int main() {
    int i;
        logAndProcess(i);               // Aufruf mit lvalue
        logAndProcess(std::move(i));    // Aufruf mit rvalue
}

In der logAndProcess Funktion wird der Parameter param an die Funktion process übergeben. Wenn man nun einen lvalue übergibt erwartet man natürlich, das auch die überladene `process` Funktion für lvalues aufgerufen wird, beziehungsweise die rvalue `process` Fuktion, wenn man einen rvalue übergibt. Jedoch ist param – genau wie alle Funktionsparameter – ein lvalue. Damit nun nicht mit jedem Aufruf von logAndProcess die `process` Funktion für lvalues aufgerufen wird brauchen wir einen Mechanismus für param, der param zu einem rvalue castet, genau dann wenn logAndProcess mit einem rvalue aufgerufen wurde.

Woher weiß nun forward, das es mit einem rvalue aufgerufen wurde? Nach den Reference Collapsing Rules von C++11 zerfallen mehrere Referenz zu einer einzigen:

  1. T&  &   ==>  T&
  2. T&  &&  ==>  T&
  3. T&& &   ==>  T&
  4. T&& &&  ==>  T&&

Betrachten wir nun unsere Funktion:

void logAndProcess(T&& param)

Ruft man die Template Funktion nun mit einem rvalue auf, also zB. ein interger Wert → logAndProcess(std::move(i)); wird der Typ <T> durch int ersetzt. Dadurch ist der Typ nun int&&. Da das Argument ein Rvalue (&&) ist, ist der Komplette Ausdruck in dem runden Klammern: int&& &&. Durch die reference collapsing rules wird dieser Ausdruck zu int&& reduziert und eine Rvalue referenz wird an die foo() Funktion übergeben.

Ruft man die Template Funktion hingegen mit einem lvalue auf, so wird nach den reference collapsing rules der Typ <T> wider durch eine Referenz ersetzt: z.B. int i; → logAndProcess(i) → int&&. Da das Argument ein Lvalue (&) ist, ist der Komplette Ausdruck in den runden Klammern: int&& &, welcher zu einer lvalue reference (int&) zerfällt.

  Wenn param mit einer rvalue reference intitialisiert wird, ist T&& eine rvalue reference.
  logAndProcess(i); =>  <T> is replaced by int&& =>  (T&& param) = int&& && param= int&& param
  Wenn param mit einer lvalue reference intitialisiert wird, ist T&& eine lvalue reference.
  logAndProcess(std::move(i)); =>  <T> is repaced by int&  =>  (T&& param) = int&&  & param= int& param

Mit perfect forwarding wird also das forwarding Problem umgangen, das auftritt wenn man eine generische Funktion schreibt, die Referenzen als Parameter annimmt.


Die folgenden Beispiele sollen noch einmal verdeutlichen, das nur mit verwendung der std::forwad Funktion alle Argumente unverändert an eine Zielfunktion (im Beispiel die Funktion foo() ) weitergereicht werden können. Nimmt zum Beispiel eine generische Funktion als Parameter eine const T& („const Referenz“), dann kann die aufgerufene Funktion den Wert des Parameters nicht verändern.

template< typename T >
void relay(const T& arg) {         // argument forwarding to foo()
    foo( arg );                    // arg can not be changed!
}

int main() {
    int reusable = 4;
    relay(reusable);               // called with lvalue ->  Works fine!
    Relay(4);                      // called with rvalue ->  Does not work!
}

Auch wenn die Funktion eine nicht const T& („Referenz“) nimmt, kann sie nicht über einen Rvalue (T&&) aufgerufen werden.

template< typename T >
void relay(T& arg) {               // arg. forwarding to foo()
    foo( arg );
}

int main() {
    int reusable = 4;
    relay(reusable);                // called with lvalue ->  Works fine!
    Relay(4);                       // called with rvalue ->  Does not work!
}

Eine gute Lösung für dieses Problem bietet das perfect forwarding. Hierbei übernimmt die template Funktion jeden möglichen Parameter und reicht ihn an an die foo() Funktion weiter, als hätte man die foo() Funktion direkt mit dem Parameter aufgerufen.

template< typename T >
void relay(T&& arg) {               // arg. forwarding to foo()
    foo( std::forward<T>(arg) );    // forward() casts arg to type of T&&.
}

int main() {
    int reusable = 4;
    relay(reusable);                // called with lvalue -> int copy ctor will be invoked
    relay(createInt());             // called with rvalue -> int move ctor will be invoked
}

Motivation für die Einführung

Dank der Einführung von rvalue reference && ist eine Unterscheidung zwischen rvalues und lvalues möglich. Von rvalues kann man keine Adresse bekommen, von lvalues schon.

Bsp:

&int i; // gibt Adresse von lvalue i zurück.

&3; // kann keine Adresse von rvalue 3 zurück geben.

Vorherige Lösungsansätze

Da es vor C++11 das Konzept „perfect forwarding“ nicht gab, konnte man sich nur über eine „zu Fuß“ Lösung behelfen: Hierzu musste man die template Funktionen für alle möglichen Eingabeparameter überladen: Bsp.:

template< typename T >
void relay(T& arg) {                // für nicht const lvalue
    foo( arg );
}
template< typename T >
void relay(T&& arg) {               // für nicht const rvalue
    foo( arg );
}
template< typename T >
void relay(const T& arg) {          // für const lvalue
    foo( arg );
}
template< typename T >
void relay(const T&& arg) {         // für const rvalue
    foo( arg );
}

Warnhinweise

Hinweis zu move semantics:

Wie man im ersten Beispiel zu move semantics sehen konnte, kann es passieren das man nicht den Effekt Erzielt den man intuitiv zu erzielen glaubt. Im Beispiel wurde die Variable text nicht in die variable value `gemoved`, sondern es wurde der copy Konstruktor der Klasse string aufgerufen. Um tatsächlich kopier Operationen zu vermeiden ist also eine Grundlegende Kenntnis der move semantics und von perfect forwarding notwendig.

Hinweis zu perfect forwarding:

Guckt man sich die Implementierung von std::forward<T>(arg) an, so sieht man diese ein übergebenes Argument zu einer T&& reference castet, also zu genau dem Typ <T> der an die template Funktion übergeben wurde (egal ob rvalue, lvalue, const oder non-const). Diesen Effekt erreicht man nicht wenn man das std::move<T>(arg) Template verwendet, da diese das übergebene Argument immer zu einem rvalue castet.

Implementierung des std::forward() Templates in utility.hpp :

  template <class T>
  constexpr T&& forward(remove_reference_t<T>& t) noexcept;
  template <class T>
  constexpr T&& forward(remove_reference_t<T>&& t) noexcept;
 

template <class T>
T&& forward(typname remove_reference<T>::type& arg) {
  return static_cast<T&&>(arg);
}

Code-Beispiele

Beispiel für die Verwendung eines move-Konstruktors:

#include <iostream>
#include <string>
#include <vector>
using namespace std;

struct A {
    //default visibility is public
    int *ptr;
    A() {
        std::cout << "Ctor" << '\n';
        ptr = new int;                      //First allocation
    }

    /* When assinging a variable to an object that has the same
       type as the variable, the copy ctor gets called */
    A(const A &other) {                     //deep copy by reference
        std::cout << "Copy-Ctor" << '\n';
        this->ptr = new int;                //Second allocation
        *this->ptr = *other.ptr;            //now "deep copy" the values of other
    }
    /* Using Move-Ctor will save the memory allocation from Copy-Ctor */
    A(A && a1) {
        std::cout << "Move-Ctor" << '\n';
        this->ptr = a1.ptr;                 //transfer the ownership
        a1.ptr = nullptr;                   //a1.ptr will not be deleted
    }
    ~A() {
        std::cout << "Dtor" << '\n';
        delete ptr;                         //delet as we allocated memory with new
    }
};

int main()
{
   vector<A> v1;
   v1.push_back(A());

   return 0;
}


Beispiel für perfect forwading:

#include <iostream>
#include <stdio.h>
using namespace std;

void foo (int arg){};
//int alread has a move and copy ctor.

int createInt (){return 5;};

/* the rvalue is forwarded as rvalue
   and the lvalue as an lvalue */
template< typename T >
void relaySimple(T arg) {         // arg. forwarding to foo()
    foo( arg );
    std::cout << "arg:" << arg << '\n';
}

/* Reference Collapsing Rules of C++11:
   1. T&  &  ==>  T&
   2. T&  && ==>  T&
   3. T&& &  ==>  T&
   4. T&& && ==>  T&&
*/

/* If arg is initialized with an rvalue, then T&& is an rvalue reference:
   relay(5); =>  <T> is replaced by int&& =>  (T&& arg) = int&& && arg= int&& arg
   If arg is initialized with an lvalue, then T&& is an lvalue reference:
   relay(x); =>  <T> is repaced by int&  =>  (T&& arg) = int&  && arg= int& arg */

/* A function using a universal reference -> which means, that this function
   can take any kind of argument: rvalue, lvalue, const, non const */
template< typename T >
void relay(T&& arg) {                   // arg. forwarding to foo()
    foo( std::forward<T>(arg) );        // forward() casts arg to type of T&&.
    std::cout << "arg:" << arg << '\n';
}


int main() {
    int reusable = 4;
    relay(reusable);    // called with lvalue -> int copy ctor will be invoked
    relay(createInt()); // called with rvalue -> int move ctor will be invoked
}


Quellenangabe:

https://docs.microsoft.com/en-us/cpp/cpp/rvalue-reference-declarator-amp-amp?view=vs-2019

https://www.nosid.org/cxx11-perfect-forwarding.html

https://www.youtube.com/watch?v=IOkgBrXCtfo

https://www.youtube.com/watch?v=0xcCNnWEMgs

Buch: Effective Modern C++ von Scott Meyers (Seite 157 und Folgende)