Vorteile der Verwendung von Forward

437

std::forwardWird bei perfekter Weiterleitung verwendet, um die benannten rWertreferenzen t1und t2unbenannte rWertreferenzen zu konvertieren . Was ist der Zweck davon? Wie würde sich das auf die aufgerufene Funktion auswirken, innerwenn wir t1& t2als lWerte belassen?

template <typename T1, typename T2>
void outer(T1&& t1, T2&& t2) 
{
    inner(std::forward<T1>(t1), std::forward<T2>(t2));
}
Steveng
quelle

Antworten:

788

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_castist der constweg:

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 fmit 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? deduceStellen Sie sich vor, wir befinden uns innerhalb der Funktion und haben einen l-Wert erhalten. Dies bedeutet , dass a ein Ziel Tist A&, und daher ist der Zieltyp für die statische Umwandlung A& &&oder nur A&. Da dies xbereits ein ist A&, tun wir nichts und bleiben mit einer Wertreferenz zurück.

Wenn uns ein r-Wert übergeben wurde, Tist der Zieltyp Afü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 fein l E-Wert empfangen wird, erhält er einen l-Wert. Wenn fein rWert empfangen wird, Eerhä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 forwardaufrufen, die dasselbe tut:

std::forward<A>(a);
// is the same as
static_cast<A&&>(a);
GManNickG
quelle
1
Wäre keine fFunktion und kein Ausdruck?
Michael Foukarakis
1
Ihr letzter Versuch ist in Bezug auf die Problemstellung nicht korrekt: Er leitet const-Werte als nicht-const weiter und leitet sie daher überhaupt nicht weiter. Beachten Sie auch, dass im ersten Versuch das const int iakzeptiert wird: Awird abgeleitet zu const int. Die Fehler betreffen die rvalues-Literale. Beachten Sie auch, dass für den Aufruf von deduced(1)x int&&nicht ist int(perfekte Weiterleitung erstellt niemals eine Kopie, wie dies bei xeinem By-Value-Parameter der Fall wäre). Nur Tist int. Der Grund, der xin der Weiterleitung als l-Wert ausgewertet wird, liegt darin, dass benannte r-Wert-Referenzen zu l-Wert-Ausdrücken werden.
Johannes Schaub - litb
5
Gibt es einen Unterschied bei der Verwendung forwardoder movehier? Oder ist es nur ein semantischer Unterschied?
0x499602D2
28
@David: std::movesollte ohne explizite Vorlagenargumente aufgerufen werden und führt immer zu einem r-Wert, std::forwardkann aber auch als solcher enden. Verwenden std::moveSie 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 Wert std::forward, der an Ihre Funktionsvorlage übergeben wird.
GManNickG
5
Vielen Dank, dass Sie zuerst mit konkreten Beispielen beginnen und das Problem motivieren. sehr hilfreich!
ShreevatsaR
60

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

Konzeptcode zur Implementierung von std :: forward

Funktion move im Code ist std::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::forwardes 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.

#include <iostream>
#include <string>
#include <utility>

void overloaded_function(std::string& param) {
  std::cout << "std::string& version" << std::endl;
}
void overloaded_function(std::string&& param) {
  std::cout << "std::string&& version" << std::endl;
}

template<typename T>
void pass_through(T&& param) {
  overloaded_function(std::forward<T>(param));
}

int main() {
  std::string pes;
  pass_through(pes);
  pass_through(std::move(pes));
}

Sicher genug, es druckt

std::string& version
std::string&& version

Der Code basiert auf einem Beispiel aus dem zuvor erwähnten Vortrag. Folie 10, ungefähr um 15:00 Uhr von Anfang an.

user7610
quelle
2
Ihr zweiter Link zeigt auf etwas völlig anderes.
Pharap
32

Bei perfekter Weiterleitung wird std :: forward verwendet, um die benannten rWertreferenzen t1 und t2 in unbenannte rWertreferenzen zu konvertieren. Was ist der Zweck davon? Wie würde sich das auf die aufgerufene innere Funktion auswirken, wenn wir t1 & t2 als lWert belassen?

template <typename T1, typename T2> void outer(T1&& t1, T2&& t2) 
{
    inner(std::forward<T1>(t1), std::forward<T2>(t2));
}

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:

void inner(int &,  int &);  // #1
void inner(int &&, int &&); // #2

Nun, wenn wir nennen outerwie diese

outer(17,29);

Wir möchten, dass 17 und 29 an # 2 weitergeleitet werden, da 17 und 29 ganzzahlige Literale und als solche rWerte sind. Aber da t1und t2im Ausdruck inner(t1,t2);lWerte sind, würden Sie # 1 anstelle von # 2 aufrufen. Deshalb müssen wir die Referenzen wieder in unbenannte Referenzen mit umwandeln std::forward. Also ist t1in outerimmer ein l-Wert-Ausdruck, während forward<T1>(t1)es abhängig von sein ein r- Wert-Ausdruck sein kann T1. Letzteres ist nur dann ein l-Wert-Ausdruck, wenn T1es sich um eine l-Wert-Referenz handelt. Und T1wird nur dann als l-Wert-Referenz abgeleitet, wenn das erste Argument für Outer ein l-Wert-Ausdruck war.

sellibitze
quelle
Dies ist eine Art verwässerte Erklärung, aber eine sehr gut gemachte und funktionale Erklärung. Die Leute sollten diese Antwort zuerst lesen und dann auf Wunsch tiefer gehen
NicoBerrogorry
11

Wie würde sich das auf die aufgerufene innere Funktion auswirken, wenn wir t1 & t2 als lWert belassen?

Wenn es sich nach dem Instanziieren T1um einen Typ charund T2eine Klasse handelt, möchten Sie t1pro Kopie und t2pro constReferenz ü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 der constZeiger 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.

sbi
quelle
4

Ein Punkt, der kristallklar gemacht hat , ist , dass nicht static_cast<T&&>Griffe const T&richtig zu.
Programm:

#include <iostream>

using namespace std;

void g(const int&)
{
    cout << "const int&\n";
}

void g(int&)
{
    cout << "int&\n";
}

void g(int&&)
{
    cout << "int&&\n";
}

template <typename T>
void f(T&& a)
{
    g(static_cast<T&&>(a));
}

int main()
{
    cout << "f(1)\n";
    f(1);
    int a = 2;
    cout << "f(a)\n";
    f(a);
    const int b = 3;
    cout << "f(const b)\n";
    f(b);
    cout << "f(a * b)\n";
    f(a * b);
}

Produziert:

f(1)
int&&
f(a)
int&
f(const b)
const int&
f(a * b)
int&&

Beachten Sie, dass 'f' eine Vorlagenfunktion sein muss. Wenn es nur als 'void f (int && a)' definiert ist, funktioniert dies nicht.

lalawawa
quelle
Guter Punkt, also folgt T && in der statischen Besetzung auch den Regeln zum Reduzieren von Referenzen, oder?
Barney
2

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.

     std::forward<int>(1);
     std::forward<std::string>("Hello");

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.

Colin
quelle
Ich glaube nicht, dass das C ++ - Komitee der Meinung ist, dass es an ihnen liegt, die Sprachsprachen "richtig" zu verwenden oder sogar zu definieren, was "richtige" Verwendung ist (obwohl sie sicherlich Richtlinien geben können). Zu diesem Zweck haben die Lehrer, Chefs und Freunde einer Person möglicherweise die Pflicht, sie auf die eine oder andere Weise zu steuern. Ich glaube jedoch, dass das C ++ - Komitee (und damit der Standard) diese Pflicht nicht hat.
SirGuy
Ja, ich habe gerade N2951 gelesen und bin damit einverstanden, dass das Standardkomitee nicht verpflichtet ist, unnötige Einschränkungen hinsichtlich der Verwendung einer Funktion hinzuzufügen. Die Namen dieser beiden Funktionsvorlagen (Verschieben und Weiterleiten) sind in der Tat etwas verwirrend, da nur ihre Definitionen in der Bibliotheksdatei oder in der Standarddokumentation (23.2.5 Helfer zum Weiterleiten / Verschieben) angezeigt werden. Die Beispiele im Standard helfen definitiv dabei, das Konzept zu verstehen, aber es kann nützlich sein, weitere Bemerkungen hinzuzufügen, um die Dinge etwas klarer zu machen.
Colin