CPP2019SSF10: Unterschied zwischen den Versionen

Aus Verteilte Systeme - Wiki
Wechseln zu: Navigation, Suche
Zeile 276: Zeile 276:
  
 
'''Quellenangabe:'''
 
'''Quellenangabe:'''
 +
 
https://docs.microsoft.com/en-us/cpp/cpp/rvalue-reference-declarator-amp-amp?view=vs-2019
 
https://docs.microsoft.com/en-us/cpp/cpp/rvalue-reference-declarator-amp-amp?view=vs-2019
  

Version vom 1. Juli 2019, 14:57 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

  1. move semantics
  2. perfect forwarding

1) move semantics: 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