Wie funktioniert std::forward? [duplicate]

Lesezeit: 9 Minuten

Benutzer-Avatar
David

Mögliches Duplikat:

Vorteile der Verwendung von forward

Ich weiß, was es tut und wann ich es benutze, aber ich still kann mir nicht vorstellen, wie es funktioniert. Bitte seien Sie so detailliert wie möglich und erklären Sie wann std::forward wäre falsch, wenn es erlaubt wäre, die Ableitung von Vorlagenargumenten zu verwenden.

Ein Teil meiner Verwirrung ist folgendes: “Wenn es einen Namen hat, ist es ein Wert” – wenn das der Fall ist, warum std::forward Verhalten sich anders, wenn ich passiere thing&& x vs thing& x?

  • Hier beantwortet. std::forward ist wirklich nur syntaktischer Zucker vorbei static_cast<T&&>.

    – Nicol Bolas

    15. Dezember 2011 um 21:19 Uhr


  • Die kurze Antwort, warum Sie den Typ nicht ableiten lassen können: im Körper von template <typename T> void foo(T && x);die Art von x ist nicht dasselbe wie was auch immer T wird abgeleitet als.

    – Kerrek SB

    15. Dezember 2011 um 22:49 Uhr


Benutzer-Avatar
Bartosz Milewski

Ich denke, die Erklärung von std::forward als static_cast<T&&> ist verwirrend. Unsere Intuition für eine Umwandlung ist, dass sie einen Typ in einen anderen Typ konvertiert – in diesem Fall wäre es eine Konvertierung in eine Rvalue-Referenz. Es ist nicht! Wir erklären also eine mysteriöse Sache mit einer anderen mysteriösen Sache. Diese spezielle Besetzung wird durch eine Tabelle in Xeos Antwort definiert. Aber die Frage ist: Warum? Also hier ist mein Verständnis:

Angenommen, ich möchte Ihnen eine weitergeben std::vector<T> v die Sie in Ihrer Datenstruktur als Datenelement speichern sollen _v. Die naive (und sichere) Lösung wäre, den Vektor immer an seinen endgültigen Bestimmungsort zu kopieren. Wenn Sie dies also über eine Zwischenfunktion (Methode) tun, sollte diese Funktion als Referenz deklariert werden. (Wenn Sie deklarieren, dass ein Vektor nach Wert genommen wird, führen Sie eine zusätzliche, völlig unnötige Kopie durch.)

void set(const std::vector<T> & v) { _v = v; }

Das ist alles in Ordnung, wenn Sie einen lvalue in der Hand haben, aber was ist mit einem rvalue? Angenommen, der Vektor ist das Ergebnis des Aufrufs einer Funktion makeAndFillVector(). Wenn Sie einen direkten Auftrag ausgeführt haben:

_v = makeAndFillVector();

der Compiler würde Bewegung den Vektor, anstatt ihn zu kopieren. Aber wenn Sie einen Vermittler einführen, set()würden die Informationen über die Rvalue-Natur Ihres Arguments verloren gehen und eine Kopie würde erstellt.

set(makeAndFillVector()); // set will still make a copy

Um diese Kopie zu vermeiden, benötigen Sie eine „perfekte Weiterleitung“, die jedes Mal zu optimalem Code führen würde. Wenn Sie einen L-Wert erhalten, soll Ihre Funktion ihn als L-Wert behandeln und eine Kopie erstellen. Wenn Sie einen R-Wert erhalten, möchten Sie, dass Ihre Funktion ihn als R-Wert behandelt und verschiebt.

Normalerweise würden Sie dies tun, indem Sie die Funktion überladen set() getrennt für lvalues ​​und rvalues:

set(const std::vector<T> & lv) { _v = v; }
set(std::vector<T> && rv) { _v = std::move(rv); }

Aber stellen Sie sich jetzt vor, Sie schreiben eine Vorlagenfunktion, die akzeptiert T und ruft set() damit T (Mach dir keine Sorgen darüber, dass unsere set() ist nur für Vektoren definiert). Der Trick besteht darin, dass dieses Template die erste Version von aufrufen soll set() wenn die Vorlagenfunktion mit einem lvalue instanziiert wird, und die zweite, wenn sie mit einem rvalue initialisiert wird.

Zunächst einmal, was sollte die Signatur dieser Funktion sein? Die Antwort lautet:

template<class T>
void perfectSet(T && t);

Je nachdem, wie Sie diese Vorlagenfunktion aufrufen, wird der Typ T etwas magisch anders abgeleitet werden. Wenn Sie es mit einem lvalue aufrufen:

std::vector<T> v;
perfectSet(v);

der Vektor v wird per Referenz übergeben. Aber wenn Sie es mit einem Rvalue aufrufen:

perfectSet(makeAndFillVector());

der (anonyme) Vektor wird per Rvalue-Referenz übergeben. Die C++11-Magie ist also absichtlich so eingerichtet, dass die Rvalue-Natur von Argumenten nach Möglichkeit erhalten bleibt.

Jetzt möchten Sie in perfectSet das Argument perfekt an die richtige Überladung von übergeben set(). Das ist wo std::forward ist notwendig:

template<class T>
void perfectSet(T && t) {
    set(std::forward<T>
}

Ohne std::forward müsste der Compiler davon ausgehen, dass wir t als Referenz übergeben wollen. Um sich davon zu überzeugen, vergleichen Sie diesen Code:

void perfectSet(T && t) {
    set
    set
}

dazu:

void perfectSet(T && t) {
    set(std::forward<T>
    set
}

Wenn Sie nicht explizit weiterleiten t, muss der Compiler defensiv davon ausgehen, dass Sie möglicherweise erneut auf t zugreifen, und die lvalue-Referenzversion von set auswählen. Aber wenn Sie weiterleiten tbehält der Compiler die Rvalue-Eigenschaft und die Rvalue-Referenzversion von bei set() wird angerufen werden. Diese Version verschiebt den Inhalt von twas bedeutet, dass das Original leer wird.

Diese Antwort ist viel länger geworden, als ich anfangs angenommen hatte 😉

  • void set(**const** std::vector & v) { _v = v; } Machen Sie es nicht komplizierter, als es sein muss.

    – Howard Hinnant

    16. Dezember 2011 um 0:21 Uhr

  • “In diesem Fall wäre es eine Konvertierung in eine Rvalue-Referenz. Das ist es nicht!” – Ja ist es! In deinem perfectSet, t bereits ist ein lvalue. Mit dem static_cast (oder std::forward), ändern wir es zurück in einen rvalue.

    – Xeo

    16. Dezember 2011 um 0:23 Uhr

  • @Xeo: Außer wenn Sie perfectSet mit einem Verweis auf vector aufrufen. Wie in: Vektor v; Vektor & vr; perfectSet(vr); Wenn Sie eine lvalue-Referenz in eine rvalue-Referenz umwandeln, ist das Ergebnis immer noch eine lvalue-Referenz. Das ist es was ich meinte.

    – Bartosz Milewski

    16. Dezember 2011 um 18:49 Uhr

  • @Bartosz: Selbst dann wandeln Sie nicht in eine rvalue-Referenz um. Wie ich in meiner Antwort sagte, werfen Sie einfach auf eine Lvalue-Referenz, eine No-Op. Die Regeln zum Zusammenklappen von Referenzen regeln das.

    – Xeo

    16. Dezember 2011 um 18:50 Uhr

  • Wie formatiert man Code in Kommentaren? Der Einrückungstrick hat nicht funktioniert.

    – Bartosz Milewski

    16. Dezember 2011 um 19:02 Uhr


Benutzer-Avatar
Xeo

Schauen wir uns zuerst an, was std::forward macht nach der Norm:

§20.2.3 [forward] p2

Kehrt zurück: static_cast<T&&>

(Woher T ist der explizit angegebene Vorlagenparameter und t ist das übergebene Argument.)

Erinnern Sie sich jetzt an die Regeln zum Zusammenklappen von Referenzen:

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)

(Schamlos aus dieser Antwort gestohlen.)

Und dann werfen wir einen Blick auf eine Klasse, die perfekte Weiterleitung einsetzen möchte:

template<class T>
struct some_struct{
  T _v;
  template<class U>
  some_struct(U&& v)
    : _v(static_cast<U&&>(v)) {} // perfect forwarding here
                                 // std::forward is just syntactic sugar for this
};

Und nun ein Beispielaufruf:

int main(){
  some_struct<int> s1(5);
  // in ctor: '5' is rvalue (int&&), so 'U' is deduced as 'int', giving 'int&&'
  // ctor after deduction: 'some_struct(int&& v)' ('U' == 'int')
  // with rvalue reference 'v' bound to rvalue '5'
  // now we 'static_cast' 'v' to 'U&&', giving 'static_cast<int&&>(v)'
  // this just turns 'v' back into an rvalue
  // (named rvalue references, 'v' in this case, are lvalues)
  // huzzah, we forwarded an rvalue to the constructor of '_v'!

  // attention, real magic happens here
  int i = 5;
  some_struct<int> s2(i);
  // in ctor: 'i' is an lvalue ('int&'), so 'U' is deduced as 'int&', giving 'int& &&'
  // applying the reference collapsing rules yields 'int&' (& + && -> &)
  // ctor after deduction and collapsing: 'some_struct(int& v)' ('U' == 'int&')
  // with lvalue reference 'v' bound to lvalue 'i'
  // now we 'static_cast' 'v' to 'U&&', giving 'static_cast<int& &&>(v)'
  // after collapsing rules: 'static_cast<int&>(v)'
  // this is a no-op, 'v' is already 'int&'
  // huzzah, we forwarded an lvalue to the constructor of '_v'!
}

Ich hoffe, diese Schritt-für-Schritt-Antwort hilft Ihnen und anderen zu verstehen, wie std::forward funktioniert.

  • "(Schamlos aus dieser Antwort gestohlen.)" Denken Sie nicht weiter darüber nach. Sie haben es von hier gestohlen: open-std.org/jtc1/sc22/wg21/docs/papers/2002/… 🙂

    – Howard Hinnant

    15. Dezember 2011 um 23:14 Uhr

  • Meine wirkliche Verwirrung war, warum std::forward keine Vorlagenargumentableitung verwenden darf, aber ich wollte es nicht in diesen Worten fragen, weil ich es bereits einmal zuvor ohne Ergebnisse versucht habe, die ich verstehen konnte. Ich glaube, ich habe es jetzt herausgefunden (stackoverflow.com/a/8862379/369872)

    – David

    14. Januar 2012 um 13:25 Uhr


  • Ich denke, das letzte Code-Snippet wird nicht funktionieren, weil Sie einen primitiven Typ int verwendet haben, der keinen Bewegungskonstruktor hat. Sie sollten etwas wie std::vector oder eine Zeichenfolge verwenden, die einen Bewegungskonstruktor hat.

    – Johnny Pauling

    14. April 2013 um 10:50 Uhr

  • Ich denke, ich werde es nie erfahren, aber ... warum die Ablehnung? Habe ich etwas verpasst? Habe ich etwas Falsches gesagt?

    – Xeo

    26. März 2014 um 10:46 Uhr

  • Ich würde dir einen Blumenkorb und eine riesige Tafel Schokolade schenken. Danke!

    – NicoBerrogorry

    27. August 2017 um 23:14 Uhr

Benutzer-Avatar
Welpe

Es funktioniert, weil beim Aufrufen der perfekten Weiterleitung der Typ T ist nicht der Werttyp, es kann auch ein Referenztyp sein.

Zum Beispiel:

template<typename T> void f(T&&);
int main() {
    std::string s;
    f(s); // T is std::string&
    const std::string s2;
    f(s2); // T is a const std::string&
}

So wie, forward Sie können sich einfach den expliziten Typ T ansehen, um zu sehen, was Sie tun Ja wirklich bestanden haben. Natürlich ist die genaue Implementierung, wenn ich mich erinnere, nicht trivial, aber dort sind die Informationen.

Wenn Sie sich auf a beziehen benannte rvalue-Referenz, dann ist das tatsächlich ein lvalue. Jedoch, forward erkennt durch die oben genannten Mittel, dass es sich tatsächlich um einen rvalue handelt, und gibt korrekt einen weiterzuleitenden rvalue zurück.

  • Aha! Können Sie weitere Beispiele (und was T ist) für std::string &s, std::string&& s, const std::string&& s, std::string* s, std::string* const s hinzufügen?

    – David

    15. Dezember 2011 um 21:42 Uhr

  • @Dave: Nein, nicht wirklich. Es gibt viele Tutorials, die sich eingehender mit dem Collapsen von Referenzen befassen.

    - Welpe

    15. Dezember 2011 um 21:54 Uhr

1013320cookie-checkWie funktioniert std::forward? [duplicate]

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy