Unterschied zwischen std :: reference_wrapper und einfachem Zeiger?

99

Warum muss es sein std::reference_wrapper? Wo soll es verwendet werden? Wie unterscheidet es sich von einem einfachen Zeiger? Wie ist seine Leistung im Vergleich zu einem einfachen Zeiger?

Laurynas Lazauskas
quelle
4
Es ist im Grunde ein Zeiger, den Sie .mit anstelle von->
MM
5
@ MM Nein, die Verwendung .funktioniert nicht so, wie Sie es vorschlagen (es sei denn, irgendwann wird der Vorschlag für den Operatorpunkt angenommen und integriert :))
Columbo,
3
Es sind Fragen wie diese, die mich unglücklich machen, wenn ich mit dem neuen C ++ arbeiten muss.
Nils
Um Columbo weiterzuverfolgen, wird std :: reference_wrapper mit seiner get()Member-Funktion oder mit seiner impliziten Konvertierung zurück in den zugrunde liegenden Typ verwendet.
Max Barraclough

Antworten:

88

std::reference_wrapperist nützlich in Kombination mit Vorlagen. Es umschließt ein Objekt, indem es einen Zeiger darauf speichert, wodurch eine Neuzuweisung und ein Kopieren ermöglicht werden, während die übliche Semantik nachgeahmt wird. Außerdem werden bestimmte Bibliotheksvorlagen angewiesen, Referenzen anstelle von Objekten zu speichern.

Betrachten Sie die Algorithmen in der STL, die Funktoren kopieren: Sie können diese Kopie vermeiden, indem Sie einfach einen Referenz-Wrapper übergeben, der sich auf den Funktor anstelle des Funktors selbst bezieht:

unsigned arr[10];
std::mt19937 myEngine;
std::generate_n( arr, 10, std::ref(myEngine) ); // Modifies myEngine's state

Das funktioniert, weil…

  • reference_wrapperS Überlastung,operator() so dass sie genau wie die Funktionsobjekte aufgerufen werden können, auf die sie sich beziehen:

    std::ref(myEngine)() // Valid expression, modifies myEngines state
  • … (Un) wie bei normalen Referenzen wird beim Kopieren (und Zuweisen) reference_wrappersnur der Pointee zugewiesen.

    int i, j;
    auto r = std::ref(i); // r refers to i
    r = std::ref(j); // Okay; r refers to j
    r = std::cref(j); // Error: Cannot bind reference_wrapper<int> to <const int>

Das Kopieren eines Referenz-Wrappers entspricht praktisch dem Kopieren eines Zeigers, der so billig wie möglich ist. Alle Funktionsaufrufe, die mit der Verwendung verbunden sind (z. B. die zu operator()), sollten nur als Inline-Aufrufe eingefügt werden.

reference_wrappers werden erstellt über std::refundstd::cref :

int i;
auto r = std::ref(i); // r is of type std::reference_wrapper<int>
auto r2 = std::cref(i); // r is of type std::reference_wrapper<const int>

Das Template-Argument gibt den Typ und die Lebenslaufqualifikation des Objekts an, auf das verwiesen wird. r2bezieht sich auf a const intund gibt nur einen Verweis auf const int. Aufrufe von Referenz-Wrappern mit constFunktoren rufen nur constMitgliedsfunktionen auf operator().

R-Wert-Initialisierer sind nicht zulässig, da ihre Zulassung mehr schaden als nützen würde. Da r-Werte ohnehin verschoben würden (und bei garantierter Kopierentfernung auch dies teilweise vermieden wird), verbessern wir die Semantik nicht. Wir können jedoch baumelnde Zeiger einführen, da ein Referenz-Wrapper die Lebensdauer des Pointees nicht verlängert.

Bibliotheksinteraktion

Wie bereits erwähnt, kann man anweisen make_tuple, eine Referenz im Ergebnis zu speichern, tupleindem man das entsprechende Argument durch a reference_wrapper:

int i;
auto t1 = std::make_tuple(i); // Copies i. Type of t1 is tuple<int>
auto t2 = std::make_tuple(std::ref(i)); // Saves a reference to i.
                                        // Type of t2 is tuple<int&>

Beachten Sie, dass sich dies geringfügig unterscheidet von forward_as_tuple: Hier sind rWerte als Argumente nicht zulässig.

std::bindzeigt das gleiche Verhalten: Es wird das Argument nicht kopiert, sondern eine Referenz gespeichert, wenn es eine ist reference_wrapper. Nützlich, wenn dieses Argument (oder der Funktor!) Nicht kopiert werden muss, sondern im Gültigkeitsbereich bleibt, während der bindFunktor verwendet wird.

Unterschied zu gewöhnlichen Zeigern

  • Es gibt keine zusätzliche Ebene der syntaktischen Indirektion. Zeiger müssen dereferenziert werden, um einen Wert für das Objekt zu erhalten, auf das sie sich beziehen. reference_wrappers haben einen impliziten Konvertierungsoperator und können wie das Objekt aufgerufen werden, das sie umschließen.

    int i;
    int& ref = std::ref(i); // Okay
  • reference_wrappers haben im Gegensatz zu Zeigern keinen Nullstatus. Sie müssen entweder mit einer Referenz oder einer anderenreference_wrapper initialisiert werden .

    std::reference_wrapper<int> r; // Invalid
  • Eine Ähnlichkeit ist die flache Kopiersemantik: Zeiger und reference_wrappers können neu zugewiesen werden.

Columbo
quelle
Ist in irgendeiner Weise std::make_tuple(std::ref(i));überlegen std::make_tuple(&i);?
Laurynas Lazauskas
6
@LaurynasLazauskas Es ist anders. Letzteres, das Sie gezeigt haben, speichert einen Zeiger darauf i, keinen Verweis darauf.
Columbo
Hm ... ich denke, ich kann diese beiden immer noch nicht so gut unterscheiden, wie ich möchte ... Nun, danke.
Laurynas Lazauskas
@Columbo Wie ist ein Array von Referenz-Wrappern möglich, wenn sie keinen Nullstatus haben? Beginnen Arrays normalerweise nicht mit allen Elementen, die auf Null gesetzt sind?
Anatolyg
2
@anatolyg Was hindert Sie daran, dieses Array zu initialisieren?
Columbo
27

Es gibt mindestens zwei motivierende Zwecke std::reference_wrapper<T>:

  1. Es dient dazu, Objekten, die als Wertparameter an Funktionsvorlagen übergeben werden, eine Referenzsemantik zu geben. Beispielsweise möchten Sie möglicherweise ein großes Funktionsobjekt übergeben, an std::for_each()das der Funktionsobjektparameter als Wert übergeben wird. Um das Kopieren des Objekts zu vermeiden, können Sie verwenden

    std::for_each(begin, end, std::ref(fun));

    Das Übergeben von Argumenten std::reference_wrapper<T>für einen std::bind()Ausdruck ist weit verbreitet, um Argumente eher nach Referenz als nach Wert zu binden.

  2. Bei Verwendung eines std::reference_wrapper<T>mit std::make_tuple()dem entsprechenden Tupelelement wird T&eher ein als ein T:

    T object;
    f(std::make_tuple(1, std::ref(object)));
Dietmar Kühl
quelle
Können Sie bitte ein Codebeispiel für den ersten Fall geben?
user1708860
1
@ user1708860: du meinst anders als die angegebene ...?
Dietmar Kühl
Ich meine tatsächlichen Code, der mit dem std :: ref (Spaß) geht, weil ich nicht verstehe, wie verwendet wird (es sei denn, Spaß ist ein Objekt und keine Funktion ...)
user1708860
2
@ user1708860: Ja, höchstwahrscheinlich funhandelt es sich um ein Funktionsobjekt (dh ein Objekt einer Klasse mit einem Funktionsaufrufoperator) und nicht um eine Funktion: Wenn funes sich um eine tatsächliche Funktion handelt, std::ref(fun)haben Sie keinen Zweck und machen den Code möglicherweise langsamer.
Dietmar Kühl
23

Ein weiterer Unterschied in Bezug auf reference_wrapperselbstdokumentierenden Code besteht darin, dass die Verwendung eines Codes das Eigentum an dem Objekt im Wesentlichen ablehnt. Im Gegensatz dazu unique_ptrbehauptet ein Ass den Besitz, während ein bloßer Zeiger möglicherweise im Besitz ist oder nicht (es ist nicht möglich zu wissen, ohne viele verwandte Codes zu betrachten):

vector<int*> a;                    // the int values might or might not be owned
vector<unique_ptr<int>> b;         // the int values are definitely owned
vector<reference_wrapper<int>> c;  // the int values are definitely not owned
Edward Loper
quelle
3
Sofern es sich nicht um Code vor C ++ 11 handelt, sollte das erste Beispiel optionale, nicht besessene Werte enthalten, z. B. für eine auf dem Index basierende Cache-Suche. Es wäre schön, wenn std uns einen Standard zur Verfügung stellen würde, der einen Wert ungleich Null darstellt (eindeutige und gemeinsam genutzte Varianten)
Bwmat
Es ist vielleicht nicht so wichtig in C ++ 11, wo nackte Zeiger sowieso fast immer geliehene Werte sind.
Elling
reference_wrapperist rohen Zeigern nicht nur überlegen, weil klar ist, dass es nicht im Besitz ist, sondern auch, weil es nicht sein kann nullptr(ohne Spielereien) und Benutzer wissen, dass sie nicht bestehen können nullptr(ohne Spielereien) und Sie wissen, dass Sie es nicht müssen überprüfe es.
underscore_d
19

Sie können es sich als praktischen Wrapper für Referenzen vorstellen, damit Sie sie in Containern verwenden können.

std::vector<std::reference_wrapper<T>> vec; // OK - does what you want
std::vector<T&> vec2; // Nope! Will not compile

Es ist im Grunde eine CopyAssignableVersion von T&. Jedes Mal, wenn Sie eine Referenz wünschen, diese aber zuweisbar sein, verwenden std::reference_wrapper<T>oder ihre Hilfsfunktion haben muss std::ref(). Oder verwenden Sie einen Zeiger.


Andere Macken: sizeof:

sizeof(std::reference_wrapper<T>) == sizeof(T*) // so 8 on a 64-bit box
sizeof(T&) == sizeof(T) // so, e.g., sizeof(vector<int>&) == 24

Und Vergleich:

int i = 42;
assert(std::ref(i) == std::ref(i)); // ok

std::string s = "hello";
assert(std::ref(s) == std::ref(s)); // compile error
Barry
quelle
1
@LaurynasLazauskas Man könnte Funktionsobjekte, die im Wrapper enthalten sind, direkt aufrufen. Das wird auch in meiner Antwort erklärt.
Columbo
2
Da die Referenzimplementierung nur ein Hinweis ist, kann ich nicht verstehen, warum Wrapper Indirektion oder Leistungseinbußen hinzufügen
Riga,
4
Es sollte nicht mehr Indirektion als eine einfache Referenz sein, wenn es um einen Release-Code geht
Riga
3
Ich würde erwarten, dass der Compiler den trivialen reference_wrapperCode einbindet und ihn mit Code identisch macht, der einen Zeiger oder eine Referenz verwendet.
David Stone
4
@LaurynasLazauskas: std::reference_wrapperhat die Garantie, dass das Objekt niemals null ist. Betrachten Sie ein Klassenmitglied std::vector<T *>. Sie müssen den gesamten Klassencode untersuchen, um festzustellen, ob dieses Objekt jemals ein nullptrim Vektor speichern kann , während std::reference_wrapper<T>Sie mit garantiert gültige Objekte haben.
David Stone