CPP2019SSF10: Unterschied zwischen den Versionen

Aus Verteilte Systeme - Wiki
Wechseln zu: Navigation, Suche
(Use Cases)
(Use Cases)
Zeile 24: Zeile 24:
  
 
'''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.
 +
 +
 +
Ein weiteres Beispiel:
 +
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 Ergebniss 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.
 +
 +
 +
 +
Im Beispiel der struct Annotation wurde erwähnt, das das Ziel von move Semantics sei, teure kopier Operationen zu vermeiden. Optional als hier noch ein erklärungsversuch, warum teuere kopier Operationen vermieden werden können:
 +
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.
 +
 +
 +
 +
 +
 +
 +
 +
 +
 +
 +
 +
 +
 +
 +
 +
 +
 +
 +
 +
 +
 +
 +
 +
 +
 +
 +
 +
 +
 +
 +
 +
 +
  
 
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.

Version vom 24. August 2019, 11:19 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 move semantics und perfect forwading zu ermöglichen.

  1. move semantics
  2. perfect forwarding

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.


Ein weiteres Beispiel: 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 Ergebniss 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.


Im Beispiel der struct Annotation wurde erwähnt, das das Ziel von move Semantics sei, teure kopier Operationen zu vermeiden. Optional als hier noch ein erklärungsversuch, warum teuere kopier Operationen vermieden werden können: 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.


















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.

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. 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. Hiermit wird das forwarding problem umgangen, das auftritt wenn man eine generische Funktion schreibt, die Referenzen als Parameter annimmt. 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!
}

Nimmt die Funktion eine T& („Referenz“), 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
}

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&&

void relay(T&& arg)

Ruft man die Template Funktion nun mit einem rvalue auf, also zB. ein interger Wert → relay(5); 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.

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 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). Hier kann keine std::move<T>(arg) funktion verwendet werden, da diese das übergebene Argument immer zu einem rvalue castet. Implementierung der std::forward() Funktion in utility.hpp :

  // forward/move:
  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

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

int main()
{
   string s = string("h") + "e" + "ll" + "o";
   cout << s << endl;
}

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()
{
   // string s = string("h") + "e" + "ll" + "o";
   // cout << s << endl;

   vector<A> v1;
   v1.push_back(A());
   // v1.push_back(A());
   // 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