Zum Inhalt springen

Perfect Forwarding mit std::forward

Verstehe, warum benannte Rvalue-Referenzen Lvalues sind, lerne das kanonische Weiterleitungsmuster mit std::forward kennen und sieh, wie emplace_back mit variadischer Weiterleitung Objekte direkt an Ort und Stelle konstruiert.

Das Problem: benannte Rvalue-Referenzen sind Lvalues

Hier ist das grundlegende Problem, das Perfect Forwarding löst: Wenn du ein Argument als T&& entgegennimmst und an eine andere Funktion weiterreichst, macht der Name daraus ein Lvalue:

void inner(int& x)  { std::cout << "lvalue\n"; }
void inner(int&& x) { std::cout << "rvalue\n"; }

void outer(int&& x) {
    inner(x);  // ruft inner(int&) auf — x ist ein Lvalue!
}

Die Wertkategorie geht an jeder Funktionsgrenze verloren. Wenn du eine Wrapper-Funktion schreibst (Logging, Zeitmessung, Factory), musst du das Argument unter Erhalt seiner ursprünglichen Wertkategorie weiterleiten. Reichst du einfach x weiter, übergibst du immer ein Lvalue. Verwendest du std::move(x), übergibst du immer ein Rvalue. Für einen generischen Wrapper ist beides falsch.

std::forward: der bedingte Cast

std::forward(x) ist ein bedingter Cast:

- Ist T eine Lvalue-Referenz (weil ein Lvalue übergeben und T entsprechend abgeleitet wurde), liefert std::forward(x) eine Lvalue-Referenz — es wird nicht gecastet.
- Ist T keine Referenz (weil ein Rvalue übergeben wurde), liefert std::forward(x) eine Rvalue-Referenz — es castet nach T&&.

Der entscheidende Unterschied zu std::move:
- std::move castet immer nach Rvalue (bedingungslos).
- std::forward castet nur dann nach Rvalue, wenn das ursprüngliche Argument ein Rvalue war (bedingt).

std::forward sollte AUSSCHLIESSLICH mit Forwarding-Referenzen verwendet werden (T&& mit abgeleitetem T). Überall sonst einzusetzen ist ein Fehler.

Das kanonische Weiterleitungsmuster

Das Muster: Nimm eine Forwarding-Referenz T&& entgegen und verwende dann std::forward(arg), um die Wertkategorie beim Weiterreichen an eine andere Funktion zu erhalten.

perfect_forwarding.cpp
#include <iostream>
#include <string>
#include <utility>
#include <chrono>

// The function we want to wrap
void process(const std::string& s) {
    std::cout << "process(lvalue): " << s << '\n';
}
void process(std::string&& s) {
    std::cout << "process(rvalue): " << s << '\n';
    // Can steal s's resources since it's an rvalue
}

// Generic wrapper that perfectly forwards
template <typename T>
void timed_process(T&& arg) {
    auto start = std::chrono::steady_clock::now();

    // std::forward<T>(arg) preserves the original value category
    process(std::forward<T>(arg));

    auto elapsed = std::chrono::steady_clock::now() - start;
    std::cout << "Took: " << elapsed.count() << " ns\n";
}

int main() {
    std::string name = "Alice";
    timed_process(name);              // T = string&,  forwards as lvalue
    timed_process(std::string("Bob")); // T = string, forwards as rvalue
    timed_process("Charlie");          // T = const char(&)[8], forwards as lvalue
}

Variadische Weiterleitung & emplace_back

Perfect Forwarding zeigt seine wahre Stärke bei variadischen Templates. emplace_back leitet Konstruktorargumente direkt in den Container weiter und konstruiert das Objekt an Ort und Stelle — ganz ohne Kopien oder Verschiebungen.

variadic_forwarding.cpp
#include <iostream>
#include <vector>
#include <string>
#include <utility>

struct Employee {
    std::string name;
    int age;
    Employee(std::string n, int a) : name(std::move(n)), age(a) {
        std::cout << "Constructed: " << name << '\n';
    }
    Employee(const Employee&) { std::cout << "Copied\n"; }
    Employee(Employee&&) noexcept { std::cout << "Moved\n"; }
};

// Custom factory using variadic forwarding
template <typename T, typename... Args>
std::vector<T> make_vector_of(Args&&... args) {
    std::vector<T> v;
    // Fold expression (C++17) + emplace_back
    (v.emplace_back(std::forward<Args>(args)), ...);
    return v;
}

int main() {
    std::vector<Employee> team;

    // push_back: creates temporary, then MOVES it into vector
    team.push_back(Employee("Alice", 30));
    // Output: Constructed: Alice, Moved

    // emplace_back: constructs IN-PLACE — no copy, no move
    team.emplace_back("Bob", 25);
    // Output: Constructed: Bob

    std::cout << "---\n";
    // Variadic forwarding
    auto v = make_vector_of<std::string>("hello", std::string("world"), "!");
    for (const auto& s : v) std::cout << s << ' ';
    std::cout << '\n';
}

move vs. forward: den Unterschied kennen

| | std::move | std::forward |
|---|---|---|
| Art des Casts | Bedingungsloser Cast nach T&& | Bedingt — Lvalue, wenn T gleich T& ist, sonst Rvalue |
| Einsatzzweck | "Ich bin damit fertig, nimm es dir" | "Reiche das mit seiner ursprünglichen Wertkategorie weiter" |
| Kontext | Konkrete Typen, letzte Verwendung einer Variablen | Nur bei Forwarding-Referenzen in Templates |
| Gefahr | Versehentlich einen Wert verschieben, den du noch brauchst | Verwendung ohne Forwarding-Referenz (sinnlos) |

Faustregel: Verwende std::move, wenn du den Wert besitzt und ihn weggeben willst. Verwende std::forward, wenn du den Wert eines anderen nur weiterleitest.

Stolperfalle

1. Mehr als einmal weiterleiten: std::forward(arg) castet möglicherweise nach Rvalue. Leitest du dasselbe Argument an zwei Funktionen weiter, entwendet die erste womöglich seine Ressourcen, und für die zweite bleibt nur Datenmüll. Leite jedes Argument höchstens einmal weiter.

2. auto&& ist eine Forwarding-Referenz: for (auto&& elem : container) leitet die Wertkategorie ab. Für die Range-for-Schleife ist das in Ordnung, in anderen Kontexten solltest du mit auto&& aber vorsichtig sein.

3. std::forward ohne Ableitung: std::forward(s) ist nur ein getarntes std::move(s). Verwende std::forward ausschließlich mit einem abgeleiteten Template-Parameter.

4. Initialisierungslisten in geschweiften Klammern: {1, 2, 3} hat keinen Typ und kann nicht weitergeleitet werden. Weise es zuerst einer Variablen zu.

Best Practice

1. Verwende std::forward nur mit Forwarding-Referenzen (T&& mit abgeleitetem T).
2. Leite jedes Argument höchstens einmal weiter — Weiterleiten kann verschieben und die Quelle leer zurücklassen.
3. Bevorzuge emplace_back gegenüber push_back, wenn du Objekte im Container konstruierst.
4. Leite nicht in einer Schleife weiter — der erste Durchlauf entwendet womöglich das Argument.
5. Nutze if constexpr mit std::is_lvalue_reference_v, wenn du für Lvalues und Rvalues unterschiedliches Verhalten brauchst.

Wichtigste Erkenntnisse
  • Benannte Rvalue-Referenzen sind Lvalues — an jeder Funktionsgrenze geht die Wertkategorie verloren
  • std::forward castet bedingt: Ein Lvalue bleibt Lvalue, ein Rvalue bleibt Rvalue
  • std::move ist bedingungslos (immer Rvalue), std::forward ist bedingt (erhält das Original)
  • emplace_back nutzt variadische Weiterleitung und konstruiert Objekte ohne jede Kopie an Ort und Stelle
  • Leite jedes Argument höchstens einmal weiter — Weiterleiten kann Ressourcen entwenden

Quiz — Teste dein Wissen

(20 XP)

1. Was ist der Unterschied zwischen `std::move` und `std::forward`?

2. Warum ist `emplace_back` beim Konstruieren von Elementen effizienter als `push_back`?

3. Was passiert, wenn du dasselbe Argument mit `std::forward` an zwei verschiedene Funktionen weiterleitest?