Perfektes Forwarding
Teil des Abschnitts Objektorientierte Programmierung der C++-Journey von Coddy. Lektion 84 von 104.
Beim Schreiben von Template-Funktionen, die Argumente akzeptieren und sie an andere Funktionen weitergeben, tritt ein Problem auf: Wie bewahrst du, ob das ursprüngliche Argument ein Lvalue oder ein Rvalue war? Perfektes Weiterleiten löst dies, indem Argumente genau so weitergegeben werden, wie sie empfangen wurden, wobei ihre Wertkategorie erhalten bleibt.
Die entscheidenden Bestandteile sind Weiterleitungsreferenzen (in einem Template-Kontext als T&& geschrieben) und std::forward. Eine Weiterleitungsreferenz kann sowohl an Lvalues als auch an Rvalues gebunden werden, und std::forward wandelt das Argument bedingt zurück in seinen ursprünglichen Typ:
#include <iostream>
#include <utility>
void process(int& x) { std::cout << "lvalue: " << x << "\n"; }
void process(int&& x) { std::cout << "rvalue: " << x << "\n"; }
template<typename T>
void wrapper(T&& arg) {
process(std::forward<T>(arg));
}
int main() {
int n = 10;
wrapper(n); // Ruft process(int&) auf – lvalue erhalten
wrapper(20); // Ruft process(int&&) auf – rvalue erhalten
}Ohne std::forward sind benannte Parameter innerhalb der Funktion immer Lvalues, selbst wenn ein Rvalue übergeben wurde. Dies würde verhindern, dass Move-Semantik beim Weiterleiten an Konstruktoren oder andere Funktionen korrekt funktioniert.
Perfektes Weiterleiten ist unerlässlich für das Schreiben von Factory-Funktionen, Wrapper-Klassen und generischem Code, der Objekte konstruieren oder Funktionen aufrufen muss, während die Absicht des Aufrufers hinsichtlich Verschiebungen gegenüber Kopien erhalten bleibt.
Aufgabe
EinfachWir erstellen eine Factory-Funktion, die Perfect Forwarding demonstriert, indem sie Objekte konstruiert und dabei die Wertkategorie der an sie übergebenen Argumente beibehält. Du erstellst eine generische Funktion make_object, die Argumente an einen Klassenkonstruktor weiterleitet und sicherstellt, dass Lvalues kopiert und Rvalues verschoben werden.
Du organisierst deinen Code über drei Dateien:
Widget.h: Definiere eineWidget-Klasse, die nachverfolgt, wie sie ihre Daten erhält.Dein
Widgetsollte einenstd::string-Namen und einenint-Wert speichern. Stelle zwei Konstruktoren bereit:- Einen, der
const std::string&(Lvalue-Referenz) undintübernimmt: GibtWidget constructed (copy): [name]aus - Einen, der
std::string&&(Rvalue-Referenz) undintübernimmt: GibtWidget constructed (move): [name]aus und verschiebt den String
Füge außerdem eine Methode
display()hinzu, die[name]: [value]ausgibt.- Einen, der
Factory.h: Erstelle deine Factory-Funktion für Perfect Forwarding.Schreibe eine Template-Funktion
make_object, die eine Forwarding-Referenz für den Namen und einen regulärenintfür den Wert akzeptiert. Verwendestd::forward, um das Namensargument an denWidget-Konstruktor zu übergeben und dabei seine ursprüngliche Wertkategorie beizubehalten.Die Funktion sollte das konstruierte
Widgetper Wert zurückgeben.main.cpp: Lies zwei Eingaben ein:- Einen Namen (String)
- Einen Wert (Ganzzahl)
Demonstriere Perfect Forwarding, indem du zwei Widgets erstellst:
- Speichere zuerst den Namen in einer lokalen Variable und übergib ihn als Lvalue an
make_object - Übergib zweitens direkt einen temporären String (denselben Namen, verkettet mit
"_temp") als Rvalue anmake_object
Rufe nach dem Erstellen jedes Widgets seine Methode
display()auf.
Zum Beispiel mit den Eingaben Gadget und 42:
Widget constructed (copy): Gadget
Gadget: 42
Widget constructed (move): Gadget_temp
Gadget_temp: 42Mit den Eingaben Device und 100:
Widget constructed (copy): Device
Device: 100
Widget constructed (move): Device_temp
Device_temp: 100Die zentrale Erkenntnis ist, dass deine Funktion make_object nicht weiß, ob sie ein Lvalue oder Rvalue erhalten hat. Durch die Verwendung von std::forward bewahrt sie diese Information jedoch beim Aufruf des Widget-Konstruktors. Dadurch kann die Überladungsauflösung des Konstruktors die passende Version auswählen, was effiziente Verschiebungen für temporäre Objekte ermöglicht und gleichzeitig benannte Variablen sicher kopiert.
Probier es selbst
#include <iostream>
#include <string>
#include "Widget.h"
#include "Factory.h"
using namespace std;
int main() {
// Eingabe lesen
string name;
int value;
cin >> name >> value;
// TODO: Erstelle das erste Widget, indem name als L-Wert übergeben wird
// Speichere den Namen in einer lokalen Variable und übergebe ihn an make_object
// TODO: Rufe display() auf dem ersten Widget auf
// TODO: Erstelle das zweite Widget, indem ein temporärer String als R-Wert übergeben wird
// Übergebe (name + "_temp") direkt an make_object
// TODO: Rufe display() auf dem zweiten Widget auf
return 0;
}
Diese Lektion enthält ein kurzes Quiz. Starte die Lektion, um es zu beantworten und deinen Fortschritt zu speichern.
Alle Lektionen in Objektorientierte Programmierung
1Grundlagen der OOP
Externe DateienC++ Build & KompilierungHeader-Dateien & QuelldateienNamespaces & GültigkeitsbereichEinführung in OOP mit C++Klassen vs. ObjekteDer „this“-ZeigerMethoden (Member-Funktionen)Attribute (Datenmember)Grundlagen zu Ctors & DtorsRückblick – Einfacher Taschenrechner4Klassenattribute
Instanz- vs. statische MemberGetter und SetterConst-MemberfunktionenMutable-SchlüsselwortStatische Methoden und VariablenFriend-Funktionen und -KlassenZusammenfassung – Bankkontoverwaltung7Vererbung
Grundlagen der VererbungZugriffsebenen bei der VererbungAufrufreihenfolge von Ctor & DtorÜberschreiben von MethodenVirtuelle Funktionen & VTableMehrfachvererbungVirtuelle VererbungRückblick – Mitarbeiterhierarchie2Speicherverwaltung
Stack- vs. Heap-SpeicherZeiger und ReferenzenDynamischer Speicher (new/delete)Intelligente Zeiger in C++RAII in C++Rückblick – Dynamischer Array-Manager5Kapselung
Zugriffsbezeichner in C++Zugriffsbezeichner im DetailInformationsverbergungStruct vs. KlasseVerschachtelte und innere KlassenWiederholung – Studierendenverwaltungssystem8Polymorphie
Polymorphie zur Compilezeit vs. LaufzeitFunktionsüberladungVirtuelle Funktionen erneut betrachtetRein virtuelle FunktionenAbstrakte KlassenInterface-Design in C++Dynamic Casting & RTTIRückblick – Shape-Calculator3Konstruktoren & Destruktoren
StandardkonstruktorParametrisierter KonstruktorKopierkonstruktorMove-KonstruktorKonstruktor-InitialisierungslistenDelegierende KonstruktorenDestruktor im DetailDrei-/Fünf-/Null-RegelZusammenfassung – String-Klasse6Operatorüberladung
Einführung in die OperatorüberladungArithmetische OperatorüberladungVergleichsoperatorüberladungStream-OperatorenZuweisungsoperatorüberladungÜberladung der Operatoren [] und ()TypkonvertierungsoperatorenWiederholung – Matrix-Klasse9Templates
FunktionstemplatesKlassen-TemplatesTemplate-SpezialisierungVariadische TemplatesGrundlagen zu SFINAE und Type TraitsRückblick – Generischer Container12Moderne C++-Funktionen
Move-Semantik & RvaluesPerfektes ForwardingLambda-Ausdrücke im Detailstd::function & std::bindconstexpr und constevalStrukturierte Bindungenoptional, variant, anyÜbe selbstständig: Online-C++-Compiler