Zum Inhalt springen

Referenzen: Lvalue, Rvalue & Fallstricke

Erkunde Lvalue- und Rvalue-Referenzen, verstehe die Wertkategorien von C++, lerne die Regeln des Reference Collapsing und Forwarding-Referenzen kennen und vermeide Fehler durch baumelnde Referenzen.

Lvalue-Referenzen

Eine Lvalue-Referenz (T&) ist ein Alias — ein weiterer Name für ein bereits existierendes Objekt. Anders als Zeiger müssen Referenzen initialisiert werden, sie können nicht null sein und können nicht auf ein anderes Objekt umgehängt werden.

Lvalue-Referenzen binden an Lvalues — Ausdrücke, die eine Identität haben (du kannst ihre Adresse nehmen). Variablen, Array-Elemente und dereferenzierte Zeiger sind alle Lvalues. Eine nicht-const Lvalue-Referenz (int&) kann nicht an ein temporäres Objekt (Rvalue) binden, denn ein temporäres Objekt zu verändern ist fast immer ein Fehler.

Grundlagen der Lvalue-Referenz

Lvalue-Referenzen sind das Arbeitspferd von C++ — sie werden für Übergabe per Referenz, Operatorüberladung und das Vermeiden von Kopien eingesetzt.

lvalue_refs.cpp
#include <iostream>
#include <string>

void append_greeting(std::string& s) {
    s += ", welcome!";  // modifies the caller's string
}

int main() {
    int x = 10;
    int& ref = x;      // ref is an alias for x
    ref = 20;
    std::cout << x << '\n';  // 20 — x was modified through ref

    // const reference can bind to a temporary (rvalue)
    const std::string& greeting = std::string("Hello");
    // The temporary's lifetime is extended to match greeting's scope
    std::cout << greeting << '\n';  // "Hello"

    // Non-const lvalue reference CANNOT bind to rvalue:
    // std::string& bad = std::string("oops");  // COMPILER ERROR

    std::string name = "Alice";
    append_greeting(name);
    std::cout << name << '\n';  // "Alice, welcome!"
}

Wertkategorien in C++

C++ besitzt eine präzise Systematik der Wertkategorien von Ausdrücken. Sie zu verstehen ist unverzichtbar für Verschiebesemantik und Perfect Forwarding:

- lvalue — hat Identität, kann nicht implizit verschoben werden. Beispiele: Variablen, *ptr, arr[i], String-Literale.
- prvalue (reiner Rvalue) — hat keine Identität, kann verschoben werden. Beispiele: 42, x + y, std::string("temp"), eine Funktion, die per Wert zurückgibt.
- xvalue (auslaufender Wert) — hat Identität UND kann verschoben werden. Beispiele: std::move(x), static_cast(x), ein Member eines Rvalue.
- glvalue (verallgemeinerter Lvalue) — Lvalue oder Xvalue (hat Identität).
- rvalue — Prvalue oder Xvalue (kann verschoben werden).

Die entscheidende Einsicht: Eine Rvalue-Referenz-Variable ist selbst ein Lvalue — sie hat einen Namen und eine Adresse. Das ist die Quelle vieler Missverständnisse und genau der Grund, warum es std::forward gibt.

Rvalue-Referenzen (T&&)

Rvalue-Referenzen binden an Rvalues (temporäre Objekte und Objekte, auf die std::move angewendet wurde). Sie sind das Fundament der Verschiebesemantik — mit ihnen erkennst du, wann ein Objekt kurz vor der Zerstörung steht, und kannst ihm seine Ressourcen entwenden.

rvalue_refs.cpp
#include <iostream>
#include <string>
#include <utility>  // std::move

void process(const std::string& s) {
    std::cout << "lvalue: " << s << '\n';
}

void process(std::string&& s) {
    std::cout << "rvalue: " << s << '\n';
    // s is an lvalue inside this function (it has a name!)
    // To move from s, you'd need std::move(s)
}

int main() {
    std::string a = "hello";
    process(a);               // calls lvalue overload
    process(std::string("world"));  // calls rvalue overload
    process(std::move(a));    // calls rvalue overload — a is now moved-from

    // IMPORTANT: a is in a valid but unspecified state
    // Only safe operations: assign to it, destroy it
    std::cout << "a after move: '" << a << "'\n";  // likely empty
}

Reference Collapsing & Forwarding-Referenzen

Wenn Referenzen in Templates zusammentreffen, greifen die Regeln des Reference Collapsing:

- T& & kollabiert zu T&
- T& && kollabiert zu T&
- T&& & kollabiert zu T&
- T&& && kollabiert zu T&&

Die Regel ist einfach: Ist eine der beiden Referenzen eine Lvalue-Referenz, ist das Ergebnis eine Lvalue-Referenz. Nur && && ergibt &&.

Eine Forwarding-Referenz (auch universelle Referenz genannt) ist T&&, wobei T ein abgeleiteter Template-Parameter ist. Sie kann sowohl an Lvalues als auch an Rvalues binden. Auch auto&& ist eine Forwarding-Referenz. std::vector&& ist dagegen KEINE — T steht hier bereits fest. Auch const T&& ist keine Forwarding-Referenz.

Stolperfalle

Eine baumelnde Referenz verweist auf ein zerstörtes Objekt. Anders als bei Dangling-Zeigern gibt es keine nullptr-Prüfung — der Code arbeitet stillschweigend mit Datenmüll weiter oder stürzt ab.

Häufige Ursachen:

- Eine Referenz auf eine lokale Variable zurückgeben — die Variable wird zerstört, sobald die Funktion zurückkehrt.
- Erfassen per Referenz in einem Lambda, das die referenzierte Variable überlebt.
- const auto& auf ein temporäres Objekt innerhalb eines Ausdrucks — const auto& val = get_map()["key"] baumelt, wenn get_map() per Wert zurückgibt, denn die temporäre Map wird beim Semikolon zerstört.
- Range-basierte for-Schleife über ein temporäres Objekt — for (auto& x : get_vector()) ist in Ordnung (das temporäre Objekt lebt für die Dauer der Schleife), aber for (auto& x : get_wrapper().get_vector()) kann baumeln, wenn der Wrapper zerstört wird.

Lebensdauerverlängerung mit const T&

Eine const-Lvalue-Referenz kann die Lebensdauer eines temporären Objekts verlängern, aber nur bei direkter Bindung. Über Funktionsaufrufe oder Ketten von Member-Zugriffen funktioniert das NICHT.

lifetime_extension.cpp
#include <iostream>
#include <string>

std::string make_greeting() { return "Hello, World!"; }

const std::string& identity(const std::string& s) { return s; }

int main() {
    // SAFE: const ref directly bound to temporary
    const std::string& good = make_greeting();
    std::cout << good << '\n';  // OK — lifetime extended

    // DANGEROUS: lifetime extension does NOT propagate through functions
    const std::string& bad = identity(make_greeting());
    // bad is DANGLING — the temporary was destroyed after identity() returned
    // std::cout << bad << '\n';  // UNDEFINED BEHAVIOR

    // SAFE: just use a value
    std::string safe = make_greeting();  // move or copy-elision
    std::cout << safe << '\n';  // always safe
}
Wichtigste Erkenntnisse
  • Lvalue-Referenzen (T&) sind Aliase — sie müssen initialisiert werden, können nicht null sein und nicht umgehängt werden
  • const T& kann an temporäre Objekte binden und verlängert deren Lebensdauer (aber nur bei direkter Bindung)
  • Rvalue-Referenzen (T&&) binden an temporäre Objekte und ermöglichen Verschiebesemantik
  • Eine benannte Rvalue-Referenz ist selbst ein Lvalue — das zu verstehen ist entscheidend
  • Reference Collapsing: Jedes & in der Kette macht das Ergebnis zu &
  • Forwarding-Referenzen (T&& mit abgeleitetem T) binden sowohl an Lvalues als auch an Rvalues

Quiz — Teste dein Wissen

(15 XP)

1. Gegeben `void f(std::string&& s)` — welche Wertkategorie hat `s` innerhalb von `f`?

2. Welche Regel des Reference Collapsing ist korrekt?

3. Wann funktioniert die Lebensdauerverlängerung temporärer Objekte durch `const T&`?