Advanced   Java   Services rvalue-Referenzen Back Next Up Home



lvalues und rvalues

Am besten gleich ein Beispiel.

int a = 42 ;
int b = 17 + 4 ;

In dieser Situation sind a und b lvalues und 42, 17 und 4 sind rvalues. Ein lvalue ist ein benamter Speicherplatz, der im Code mit seinem Namen angesprochen werden kann. Dies kann über einen Pointer oder über eine Referenz geschehen, die selbst wieder lvalues sind.

int a = 42 ;
int b = 17 + 4 ;
int *apoi = &a;
int &lvRef = a;

Die Literale selbst haben keinen Namen belegen aber auch Speicherplatz (in der Datasection auf dem Stack). Dieser Speicherplatz ist temporär und wird nach Beendigung der Zuweisung freigegeben. Bisher war dieser temporärer Speicher nicht ansprechbar, daher reagiert der Compiler auf die beiden folgenden Statements mit Fehlermeldungen.

int *apoi = &42;
int &lvRef = 17+4;

rvalue-Referenzen

Der neue C++11 Standard ermöglicht es nun diesen Speicher mit Hilfe von rvalue-Referenzen anzusprechen. Die bisherigen Referenzen heißen deswegen nun lvalue-Referenzen. Im folgenden wird einer rvalue-Referenz temporärer Speicher zugewiesen und damit "dauerhaft" gemacht. Man kann sogar einen Pointer auf diesen Speicherplatz zeigen lassen und die rvalue-Referenz über diesen Pointer manipulieren.

int &&rvRef = 17 + 4;
cout << rvRef << endl;
int* poi = &rvRef;
cout << poi << endl;
*poi = 22;
cout << *poi << endl;

cpp-rvalue-1.jpg

Sogar eine "normale" Referenz läßt sich jetzt auf die eben eingerichtete rvalue-Referenz erzeugen und der Speicherwert darüber verändern.

int &ref = rvRef;
ref = 42;
cout << rvRef << endl;  // 42

Während man also mit Hilfe einer rvalue-Referenz temporären Speicher in persistenten Speicher verwandelt ist das umgekehrte nicht möglich, aus einem lvalue kann kein rvalue erzeugt werden.

int a = 17;
int &&rvRef = a; // NOK

Hier meldet sich der Compiler mit einer Fehlermeldung : cannot bind 'int' lvalue to 'int&&'.

Auch zu einer Konstanten kann man keine rvalue-referenz erzeugen.

const int co = 17;
int &&rvRef = co; // NOK

Hier meldet sich der Compiler mit einer Fehlermeldung : cannot convert 'const int' to 'int &&'.


Der Vorteil

Auf den ersten Blick scheint hier nichts gewonnen. Doch arbeitet int a = 17+4; ganz anders als int&& rvRef = 17 + 4;. Im ersten Fall wird Speicher angelegt für die 17 und die 4 und für das Zwischenergebnis 17+4. Dieses Zwischenergebnis wird dann in den Speicherplatz a kopiert. Im zweiten Fall wird der temporäre Speicherplatz für das Zwischenergebnis zu einem regulärem Speicherplatz. Der letzte Kopierschritt entfällt daher, was einen Performancegewinn bedeutet.


Die Anwendung

Der bestehende copy-constructor wird in C++11 durch einen move-constructor ergänzt, der mit rvalue-Referenzen arbeitet. Der Compiler selbst entscheidet dann welcher der beiden Konstruktoren in der jeweiligen Situation angewendet wird.

Ebenso wird der Zuweisungsoperator operator=() um einen speziellen Zuweisungsoperator ergänzt, der mit rvalue-Referenzen arbeitet. Auch hier entscheidet dann der Compiler welche Operatorfunktion eingesetzt wird.


rvalue-Referenzen als Funktionsparameter

Durch die Einführung von rvalue-Referenzen können diese natürlich auch als Parameter in Funktionen/Methoden auftauchen. Man bestätigt leicht folgende Regeln.

SignaturVerhalten
void foo(int x)Es können lvalues UND rvalues übergeben werden
void foo(int& x)Es können NUR lvalues übergeben werden
void foo(int&& x)Es können NUR rvalues übergeben werden

Man sieht, daß die Signaturen void foo(int& x) und void foo(int&& x) miteinander verträglich sind, eine Kombination mit void foo(int x) aber nicht.



Hier die Situation beim Returntyp und bei der Zuweisung

SignaturIn der Return-Anweisung mögliche Datentypen
int foo()(const) int, (const) int&, (const) int&&
int& foo()int, int&, int&&, keine const-Typen
int&& foo()NUR int-Literale

Ein Beispiel

Wir deklarieren vier Funktionen mit folgenden Signaturen.

void test(int i, string &id);
void test(int i, string &&id);
string returnValue();
string& returnReference();

Die beiden test-Funktionen realisieren wir wie folgt:

void test(int i, string &id)
{

   cout << i << " test(String &id)" << endl;
}

void test(int i, string &&id)
{
   cout << i << " test(String &&id)" << endl;
}

Die letzten beiden folgendermaßen.

string returnValue()
{
   return "007";
}

string& returnReference()
{
   return *(new string("007")) ;
}

main

int main()
{
   string s("007");
   test(1, returnReference() );
   test(2, returnValue() );
   test(3, s);
   test(4, string("007") );
   return 0;
}

Screenshot

cpp-rvalue-2.jpg

Im ersten Aufruf wird test eine Referenz übergeben und daher wird die Funktion gerufen, deren Signatur genau paßt (perfect match). Im zweiten Aufruf gibt returnValue eine Kopie des aus "007" erstellten Strings zurück. Bei dieser Kopie handelt es sich um einen typischen rvalue. Deshalb wird jetzt die test-Funktion mit der rvalue-Referenz in der Signatur gerufen. Im dritten Fall ist der zweite Parameter kein temporäres Objekt und daher kann keinesfalls die Funktion mit der rvalue-Referenz gerufen werden. Im vierten Fall wird bei der Übergabe ein temporäres anonymes Objekt erstellt, also diesmal die Funktion mit der rvalue-Referenz die richtige Wahl. Die beiden Signaturen der test-Funktion ergänzen sich also ideal.

Valid XHTML 1.0 Strict top Back Next Up Home