wake-up-neo.com

Vorteile der Verwendung vorwärts

Bei der perfekten Weiterleitung werden mit std::forward Die benannten Referenzwerte t1 Und t2 In unbenannte Referenzwerte konvertiert. Was ist der Zweck, das zu tun? Wie würde sich das auf die aufgerufene Funktion inner auswirken, wenn wir t1 & t2 Als lWerte belassen?

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

Sie müssen das Weiterleitungsproblem verstehen. Sie können lesen Sie das gesamte Problem im Detail , aber ich werde zusammenfassen.

Grundsätzlich möchten wir, dass der Ausdruck E(a, b, ... , c) mit dem Ausdruck f(a, b, ... , c) gleichwertig 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 kann jedoch temporäre Werte nicht verarbeiten: f(1, 2, 3);, da diese nicht an eine lWert-Referenz 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 Flips flippt. Es ist jetzt nicht mehr möglich, dass E nicht konstante Argumente hat:

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 const-Referenzen, aber dann ist const_cast Das 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 erledigt alles richtig ... auf Kosten der Unmöglichkeit, es zu warten. Sie liefern Überladungen von f mit all 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 2N 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, aber dies kann möglicherweise eine Menge Code beschädigen. Wir müssen also 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 knacken.

Wenn ein Verweis auf einen Verweis gegeben wird (Verweis ist ein umfassender Begriff, der sowohl T& Als auch T&& Bedeutet), verwenden wir die folgende Regel, um den resultierenden Typ herauszufinden:

"Wenn ein Typ TR eine Referenz auf einen Typ T ist, erstellt ein Versuch, den Typ" Wertereferenz auf CV TR "zu erstellen, den Typ" Wertereferenz auf T ", während ein Versuch, den Typ" Wertereferenz auf "zu erstellen, ausgeführt wird cv TR ”erzeugt 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 wird mit der Ableitung von Vorlagenargumenten Folgendes ausgeführt: Wenn ein Argument ein Wert A ist, wird das Vorlagenargument mit einem Wertverweis auf A versehen. Andernfalls wird normal abgeleitet. Dies ergibt so genannte Universalverweise (der Begriff Weiterleitungsverweis ist jetzt der offizielle eins).

Warum ist das nützlich? Weil wir zusammen die Möglichkeit behalten, die Wertkategorie eines Typs zu verfolgen: Wenn es ein Wert war, haben wir einen Wert-Referenz-Parameter, andernfalls haben wir einen Wert-Referenz-Parameter.

In Code:

template <typename T>
void deduce(T&& x); 

int i;
deduce(i); // deduce<int&>(int& &&) -> deduce<int&>(int&)
deduce(1); // deduce<int>(int&&)

Das Letzte ist, die Wertkategorie der Variablen "weiterzuleiten". Denken Sie daran, dass der Parameter innerhalb der Funktion als Wert an Folgendes ü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 Wertkategorie haben wie wir! Die Lösung lautet wie folgt:

static_cast<T&&>(x);

Was macht das? Stellen Sie sich vor, wir befinden uns in der Funktion deduce und haben einen l-Wert erhalten. Dies bedeutet, dass T ein A& Ist und der Zieltyp für die statische Umwandlung A& && Oder nur A& Ist. Da x bereits ein A& Ist, tun wir nichts und behalten eine Wertreferenz.

Wenn uns ein Wert übergeben wurde, ist TA, sodass der Zieltyp für die statische Umwandlung A&& Ist. Die Umwandlung führt zu einem Wertausdruck der nicht mehr an eine Wertreferenz übergeben werden kann. Wir haben die Wertkategorie des Parameters beibehalten.

Wenn wir diese zusammenstellen, erhalten wir eine "perfekte Weiterleitung":

template <typename A>
void f(A&& a)
{
    E(static_cast<A&&>(a)); 
}

Wenn f einen Wert erhält, erhält E einen Wert. Wenn f einen Wert erhält, erhält E einen Wert. Perfekt.


Und natürlich wollen wir das Hässliche loswerden. static_cast<T&&> Ist kryptisch und seltsam zu merken; Lassen Sie uns stattdessen eine Utility-Funktion namens forward erstellen, die dasselbe tut:

std::forward<A>(a);
// is the same as
static_cast<A&&>(a);
735
GManNickG

Ich denke, ein konzeptioneller Code, der std :: forward implementiert, kann zur Diskussion beitragen. Dies ist eine Folie aus Scott Meyers Vortrag An Effective C++ 11/14 Sampler

conceptual code implementing std::forward

Funktion move im Code ist std::move. Es gibt eine (funktionierende) Implementierung dafür zu einem früheren Zeitpunkt in diesem Vortrag. Ich fand tatsächliche Implementierung von std :: forward in libstdc ++ in der Datei move.h, aber es ist überhaupt nicht instruktiv.

Aus Sicht des Benutzers bedeutet dies, dass std::forward ist eine bedingte Besetzung eines Wertes. Es kann nützlich sein, wenn ich eine Funktion schreibe, die entweder einen l-Wert oder einen r-Wert in einem Parameter erwartet und sie 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 eingeschlossen hätte, 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, von Anfang an gegen 15:00 Uhr.

53
user7610

Bei der perfekten Weiterleitung wird std :: forward verwendet, um die benannten rWertreferenzen t1 und t2 in unbenannte rWertreferenzen umzuwandeln. Was ist der Zweck, das zu tun? Wie würde sich das auf die aufgerufene Funktion auswirken, wenn wir t1 & t2 als Wert 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 rvalue-Referenz verwenden, handelt es sich tatsächlich um eine lvalue-Referenz (da Sie das Objekt namentlich referenzieren). Betrachten Sie das folgende Beispiel:

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

Nun, wenn wir outer so nennen

outer(17,29);

wir möchten, dass 17 und 29 an # 2 weitergeleitet werden, da 17 und 29 ganzzahlige Literale und als solche Werte sind. Da jedoch t1 Und t2 Im Ausdruck inner(t1,t2); l-Werte sind, würden Sie anstelle von # 2 # 1 aufrufen. Deshalb müssen wir die Referenzen mit std::forward Wieder in unbenannte Referenzen umwandeln. Daher ist t1 In outer immer ein Wertausdruck, während forward<T1>(t1) je nach T1 Ein Wertausdruck sein kann. Letzteres ist nur dann ein lWert-Ausdruck, wenn T1 Eine lWert-Referenz ist. Und T1 Wird nur dann als lWert-Referenz hergeleitet, wenn das erste Argument für Outer ein lWert-Ausdruck war.

27
sellibitze

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

Wenn nach dem Instanziieren T1 Vom Typ char ist und T2 Eine Klasse ist, möchten Sie t1 Pro Kopie und t2 nach const Referenz. Nun, es sei denn, inner() nimmt sie als Nichtconst -Referenz, das heißt, in diesem Fall möchten Sie dies auch tun.

Versuchen Sie, eine Reihe von outer() - Funktionen zu schreiben, die dies ohne r-Wert-Referenzen implementieren, und leiten Sie den richtigen Weg ab, die Argumente vom Typ inner() zu übergeben. Ich denke, Sie brauchen etwas 2 ^ 2 von ihnen, 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 einer inner(), die Argumente pro Zeiger akzeptiert. Ich denke das macht jetzt 3 ^ 2. (Oder 4 ^ 2. Hölle, ich kann nicht versuchen zu überlegen, ob der Zeiger const einen Unterschied machen würde.)

Und stellen Sie sich dann vor, Sie möchten dies für fünf Parameter tun. Oder sieben.

Jetzt wissen Sie, warum einige schlaue Köpfe auf "perfekte Weiterleitung" gekommen sind: Der Compiler erledigt das alles für Sie.

11
sbi

Ein Punkt, der nicht kristallklar gemacht wurde, ist, dass static_cast<T&&> behandelt const T& auch richtig.
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.

4
lalawawa

Es kann sich lohnen, darauf hinzuweisen, dass forward zusammen mit einer äußeren Methode mit forwarding/universal reference verwendet werden muss. Die Verwendung der folgenden Anweisungen für sich allein ist zulässig, bewirkt jedoch nur Verwirrung. Das Standardkomitee möchte diese Flexibilität möglicherweise deaktivieren. Warum verwenden wir stattdessen nicht einfach static_cast?

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

Meiner Meinung nach handelt es sich bei move and forward um Entwurfsmuster, die nach Einführung des Referenztyps r-value zu natürlichen Ergebnissen führen. Wir sollten eine Methode nicht benennen, wenn sie korrekt verwendet wird, es sei denn, eine falsche Verwendung ist verboten.

2
colin