Sie müssen das Weiterleitungsproblem verstehen. Sie können das gesamte Problem im Detail lesen , aber ich werde es zusammenfassen.
Grundsätzlich E(a, b, ... , c)
wollen wir angesichts des Ausdrucks , dass der Ausdruck f(a, b, ... , c)
äquivalent ist. In C ++ 03 ist dies nicht möglich. Es gibt viele Versuche, aber alle scheitern in gewisser Hinsicht.
Am einfachsten ist es, eine Wertreferenz zu verwenden:
template <typename A, typename B, typename C>
void f(A& a, B& b, C& c)
{
E(a, b, c);
}
Dies behandelt jedoch keine temporären Werte : f(1, 2, 3);
, da diese nicht an eine Wertreferenz gebunden werden können.
Der nächste Versuch könnte sein:
template <typename A, typename B, typename C>
void f(const A& a, const B& b, const C& c)
{
E(a, b, c);
}
Was das obige Problem behebt, aber Flops flippt. Es ist jetzt nicht möglich E
, nicht konstante Argumente zu haben:
int i = 1, j = 2, k = 3;
void E(int&, int&, int&); f(i, j, k); // oops! E cannot modify these
Der dritte Versuch akzeptiert konst Verweise, aber dann const_cast
ist der const
weg:
template <typename A, typename B, typename C>
void f(const A& a, const B& b, const C& c)
{
E(const_cast<A&>(a), const_cast<B&>(b), const_cast<C&>(c));
}
Dies akzeptiert alle Werte, kann alle Werte weitergeben, führt jedoch möglicherweise zu undefiniertem Verhalten:
const int i = 1, j = 2, k = 3;
E(int&, int&, int&); f(i, j, k); // ouch! E can modify a const object!
Eine endgültige Lösung handhabt alles richtig ... auf Kosten der Unwartbarkeit. Sie bieten Überladungen von f
mit allen Kombinationen von const und non-const:
template <typename A, typename B, typename C>
void f(A& a, B& b, C& c);
template <typename A, typename B, typename C>
void f(const A& a, B& b, C& c);
template <typename A, typename B, typename C>
void f(A& a, const B& b, C& c);
template <typename A, typename B, typename C>
void f(A& a, B& b, const C& c);
template <typename A, typename B, typename C>
void f(const A& a, const B& b, C& c);
template <typename A, typename B, typename C>
void f(const A& a, B& b, const C& c);
template <typename A, typename B, typename C>
void f(A& a, const B& b, const C& c);
template <typename A, typename B, typename C>
void f(const A& a, const B& b, const C& c);
N Argumente erfordern 2 N Kombinationen, ein Albtraum. Wir möchten dies automatisch tun.
(Dies ist effektiv das, was der Compiler in C ++ 11 für uns tun soll.)
In C ++ 11 haben wir die Möglichkeit, dies zu beheben. Eine Lösung ändert die Regeln für den Vorlagenabzug für vorhandene Typen. Dadurch wird jedoch möglicherweise viel Code beschädigt. Also müssen wir einen anderen Weg finden.
Die Lösung besteht darin, stattdessen die neu hinzugefügten rvalue-Referenzen zu verwenden . Wir können neue Regeln einführen, wenn wir rWert-Referenztypen ableiten und jedes gewünschte Ergebnis erzielen. Schließlich können wir jetzt unmöglich Code brechen.
Wenn eine Referenz auf eine Referenz angegeben wird (Hinweisreferenz ist ein umfassender Begriff, der sowohl T&
als als auch bedeutet T&&
), verwenden wir die folgende Regel, um den resultierenden Typ herauszufinden:
"[gegeben] ein Typ TR, der eine Referenz auf einen Typ T ist, ein Versuch, den Typ" lWertreferenz auf Lebenslauf TR "zu erstellen, erzeugt den Typ" lWertreferenz auf T ", während ein Versuch, den Typ" rWertreferenz auf T "zu erstellen cv TR "erstellt den Typ TR."
Oder in tabellarischer Form:
TR R
T& & -> T& // lvalue reference to cv TR -> lvalue reference to T
T& && -> T& // rvalue reference to cv TR -> TR (lvalue reference to T)
T&& & -> T& // lvalue reference to cv TR -> lvalue reference to T
T&& && -> T&& // rvalue reference to cv TR -> TR (rvalue reference to T)
Als nächstes mit Abzug von Vorlagenargumenten: Wenn ein Argument ein l-Wert A ist, liefern wir dem Vorlagenargument einen l-Wert-Verweis auf A. Andernfalls leiten wir normal ab. Dies ergibt sogenannte universelle Referenzen (der Begriff Weiterleitungsreferenz ist jetzt die offizielle).
Warum ist das nützlich? Da wir zusammen die Möglichkeit behalten, die Wertekategorie eines Typs zu verfolgen: Wenn es sich um einen l-Wert handelt, haben wir einen l-Wert-Referenzparameter, andernfalls haben wir einen r-Wert-Referenzparameter.
In Code:
template <typename T>
void deduce(T&& x);
int i;
deduce(i); // deduce<int&>(int& &&) -> deduce<int&>(int&)
deduce(1); // deduce<int>(int&&)
Als letztes wird die Wertekategorie der Variablen "weitergeleitet". Denken Sie daran, dass der Parameter innerhalb der Funktion als Wert an alles übergeben werden kann:
void foo(int&);
template <typename T>
void deduce(T&& x)
{
foo(x); // fine, foo can refer to x
}
deduce(1); // okay, foo operates on x which has a value of 1
Das ist nicht gut. E muss die gleiche Art von Wertkategorie bekommen, die wir haben! Die Lösung lautet:
static_cast<T&&>(x);
Was macht das? deduce
Stellen Sie sich vor, wir befinden uns innerhalb der Funktion und haben einen l-Wert erhalten. Dies bedeutet , dass a ein Ziel T
ist A&
, und daher ist der Zieltyp für die statische Umwandlung A& &&
oder nur A&
. Da dies x
bereits ein ist A&
, tun wir nichts und bleiben mit einer Wertreferenz zurück.
Wenn uns ein r-Wert übergeben wurde, T
ist der Zieltyp A
für die statische Umwandlung A&&
. Die Umwandlung führt zu einem r-Wert-Ausdruck, der nicht mehr an eine l-Wert-Referenz übergeben werden kann . Wir haben die Wertekategorie des Parameters beibehalten.
Wenn wir diese zusammenfügen, erhalten wir eine "perfekte Weiterleitung":
template <typename A>
void f(A&& a)
{
E(static_cast<A&&>(a));
}
Wenn f
ein l E
-Wert empfangen wird, erhält er einen l-Wert. Wenn f
ein rWert empfangen wird, E
erhält er einen rWert. Perfekt.
Und natürlich wollen wir das Hässliche loswerden. static_cast<T&&>
ist kryptisch und seltsam zu erinnern; Lassen Sie uns stattdessen eine Utility-Funktion namens forward
aufrufen, die dasselbe tut:
std::forward<A>(a);
// is the same as
static_cast<A&&>(a);
f
Funktion und kein Ausdruck?const int i
akzeptiert wird:A
wird abgeleitet zuconst int
. Die Fehler betreffen die rvalues-Literale. Beachten Sie auch, dass für den Aufruf vondeduced(1)
xint&&
nicht istint
(perfekte Weiterleitung erstellt niemals eine Kopie, wie dies beix
einem By-Value-Parameter der Fall wäre). NurT
istint
. Der Grund, derx
in der Weiterleitung als l-Wert ausgewertet wird, liegt darin, dass benannte r-Wert-Referenzen zu l-Wert-Ausdrücken werden.forward
odermove
hier? Oder ist es nur ein semantischer Unterschied?std::move
sollte ohne explizite Vorlagenargumente aufgerufen werden und führt immer zu einem r-Wert,std::forward
kann aber auch als solcher enden. Verwendenstd::move
Sie diese Option, wenn Sie wissen, dass Sie den Wert nicht mehr benötigen und an einen anderen Ort verschieben möchten. Verwenden Sie dazu den Wertstd::forward
, der an Ihre Funktionsvorlage übergeben wird.Ich denke, dass ein konzeptioneller Code, der std :: forward implementiert, zur Diskussion beitragen kann. Dies ist eine Folie aus Scott Meyers Vortrag An Effective C ++ 11/14 Sampler
Funktion
move
im Code iststd::move
. Es gibt eine (funktionierende) Implementierung dafür früher in diesem Vortrag. Ich habe die tatsächliche Implementierung von std :: forward in libstdc ++ in der Datei move.h gefunden, aber es ist überhaupt nicht lehrreich.Aus der Sicht eines Benutzers bedeutet dies, dass
std::forward
es sich um eine bedingte Umwandlung in einen Wert handelt. Es kann nützlich sein, wenn ich eine Funktion schreibe, die entweder einen l-Wert oder einen r-Wert in einem Parameter erwartet und diese nur dann als r-Wert an eine andere Funktion übergeben möchte, wenn sie als r-Wert übergeben wurde. Wenn ich den Parameter nicht in std :: forward einbinden würde, würde er immer als normale Referenz übergeben.Sicher genug, es druckt
Der Code basiert auf einem Beispiel aus dem zuvor erwähnten Vortrag. Folie 10, ungefähr um 15:00 Uhr von Anfang an.
quelle
Wenn Sie in einem Ausdruck eine benannte r-Wert-Referenz verwenden, handelt es sich tatsächlich um einen l-Wert (da Sie mit Namen auf das Objekt verweisen). Betrachten Sie das folgende Beispiel:
Nun, wenn wir nennen
outer
wie dieseWir möchten, dass 17 und 29 an # 2 weitergeleitet werden, da 17 und 29 ganzzahlige Literale und als solche rWerte sind. Aber da
t1
undt2
im Ausdruckinner(t1,t2);
lWerte sind, würden Sie # 1 anstelle von # 2 aufrufen. Deshalb müssen wir die Referenzen wieder in unbenannte Referenzen mit umwandelnstd::forward
. Also istt1
inouter
immer ein l-Wert-Ausdruck, währendforward<T1>(t1)
es abhängig von sein ein r- Wert-Ausdruck sein kannT1
. Letzteres ist nur dann ein l-Wert-Ausdruck, wennT1
es sich um eine l-Wert-Referenz handelt. UndT1
wird nur dann als l-Wert-Referenz abgeleitet, wenn das erste Argument für Outer ein l-Wert-Ausdruck war.quelle
Wenn es sich nach dem Instanziieren
T1
um einen Typchar
undT2
eine Klasse handelt, möchten Siet1
pro Kopie undt2
proconst
Referenz übergeben. Naja, es sei denn,inner()
sie nehmen pro Nicht-const
, das heißt, in diesem Fall möchten Sie dies auch tun.Versuchen Sie, eine Reihe von
outer()
Funktionen zu schreiben, die dies ohne rvalue-Referenzen implementieren, und ermitteln Sie den richtigen Weg, um die Argumente zu übergebeninner()
Typ 's' zu übergeben. Ich denke, Sie brauchen etwas 2 ^ 2 davon, ziemlich umfangreiches Template-Meta-Zeug, um die Argumente abzuleiten, und viel Zeit, um dies für alle Fälle richtig zu machen.Und dann kommt jemand mit einem
inner()
, der Argumente pro Zeiger nimmt. Ich denke das macht jetzt 3 ^ 2. (Oder 4 ^ 2. Hölle, ich kann mir nicht die Mühe machen zu überlegen, ob derconst
Zeiger einen Unterschied machen würde.)Und dann stellen Sie sich vor, Sie möchten dies für fünf Parameter tun. Oder sieben.
Jetzt wissen Sie, warum einige kluge Köpfe auf "perfekte Weiterleitung" gekommen sind: Der Compiler erledigt das alles für Sie.
quelle
Ein Punkt, der kristallklar gemacht hat , ist , dass nicht
static_cast<T&&>
Griffeconst T&
richtig zu.Programm:
Produziert:
Beachten Sie, dass 'f' eine Vorlagenfunktion sein muss. Wenn es nur als 'void f (int && a)' definiert ist, funktioniert dies nicht.
quelle
Es kann sinnvoll sein zu betonen, dass Forward zusammen mit einer äußeren Methode mit Forwarding / Universal Reference verwendet werden muss. Die Verwendung von Forward für sich wie folgt ist zulässig, bringt aber nichts anderes als Verwirrung. Das Standardkomitee möchte diese Flexibilität möglicherweise deaktivieren, andernfalls verwenden wir stattdessen nicht einfach static_cast.
Meiner Meinung nach sind Move and Forward Entwurfsmuster, die natürliche Ergebnisse sind, nachdem der Referenztyp r-value eingeführt wurde. Wir sollten eine Methode nicht benennen, wenn sie korrekt verwendet wird, es sei denn, eine falsche Verwendung ist verboten.
quelle