Różnica między std :: reference_wrapper a prostym wskaźnikiem?

100

Dlaczego jest taka potrzeba std::reference_wrapper? Gdzie powinien być używany? Czym różni się od prostego wskaźnika? Jak jego wydajność wypada w porównaniu z prostym wskaźnikiem?

Laurynas Lazauskas
źródło
4
Jest to po prostu wskaźnik, którego używasz .zamiast->
MM
5
@MM Nie, używanie .nie działa tak, jak sugerujesz (chyba że w którymś momencie propozycja kropki operatora zostanie przyjęta i zintegrowana :))
Columbo
3
Takie pytania sprawiają, że jestem nieszczęśliwy, kiedy muszę pracować z nowym C ++.
Nils
Aby śledzić Columbo, std :: reference_wrapper jest używany z jego get()funkcją składową lub z jej niejawną konwersją z powrotem do typu podstawowego.
Max Barraclough

Odpowiedzi:

88

std::reference_wrapperjest przydatna w połączeniu z szablonami. Zawija obiekt, przechowując do niego wskaźnik, umożliwiając ponowne przypisanie i kopiowanie, naśladując jego zwykłą semantykę. Nakazuje również niektórym szablonom bibliotek przechowywanie odniesień zamiast obiektów.

Rozważmy algorytmy w STL, które kopiują funktory: Możesz uniknąć tej kopii, przekazując po prostu opakowanie referencyjne odnoszące się do funktora zamiast do samego funktora:

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

To działa, ponieważ…

  • reference_wrapperS przeciążenie,operator() więc można je wywołać tak jak obiekty funkcji, do których się odnoszą:

    std::ref(myEngine)() // Valid expression, modifies myEngines state
  • … (Nie) jak zwykłe odniesienia, kopiowanie (i przypisywanie) reference_wrapperspo prostu przypisuje punkt.

    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>

Kopiowanie opakowania referencyjnego jest praktycznie równoważne kopiowaniu wskaźnika, które jest tak tanie, jak to tylko możliwe. Wszystkie wywołania funkcji nieodłącznie związane z jej używaniem (np. Te do operator()) powinny być po prostu wstawiane, ponieważ są jednowierszowe.

reference_wrappers są tworzone przez std::refistd::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>

Argument szablonu określa typ i kwalifikację cv obiektu, do którego się odwołuje; r2odnosi się do a const inti zwróci tylko odniesienie do const int. Wywołania opakowań referencyjnych z constfunktorami będą wywoływać tylko constfunkcje operator()składowe s.

Inicjatory Rvalue są niedozwolone, ponieważ pozwolenie im przyniosłoby więcej szkody niż pożytku. Ponieważ rvalues ​​i tak zostałyby przeniesione (i przy gwarantowanej eliminacji kopiowania, nawet jeśli jest to częściowo unikane), nie poprawiamy semantyki; możemy jednak wprowadzić wiszące wskaźniki, ponieważ opakowanie odniesienia nie przedłuża jego życia.

Interakcja biblioteki

Jak wspomniano wcześniej, można poinstruować, make_tupleaby zapisać odwołanie w wyniku tuple, przekazując odpowiedni argument przez 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&>

Zauważ, że to nieco różni się od forward_as_tuple: Tutaj rwartości jako argumenty nie są dozwolone.

std::bindpokazuje to samo zachowanie: nie skopiuje argumentu, ale zapisze odniesienie, jeśli jest to plik reference_wrapper. Przydatne, jeśli ten argument (lub funktor!) Nie musi być kopiowany, ale pozostaje w zakresie, gdy bindużywany jest -functor.

Różnica w stosunku do zwykłych wskaźników

  • Nie ma dodatkowego poziomu pośrednictwa syntaktycznego. Wskaźniki muszą zostać wyłuskane, aby uzyskać lwartość dla obiektu, do którego się odnoszą; reference_wrappers mają niejawny operator konwersji i mogą być wywoływane jak zawijany obiekt.

    int i;
    int& ref = std::ref(i); // Okay
  • reference_wrappers, w przeciwieństwie do wskaźników, nie mają stanu zerowego. Muszą zostać zainicjowane za pomocą odwołania lub innegoreference_wrapper .

    std::reference_wrapper<int> r; // Invalid
  • Podobieństwo występuje w semantyce płytkiej kopii: wskaźniki i reference_wrappers można ponownie przypisać.

Columbo
źródło
W jakiś sposób jest std::make_tuple(std::ref(i));lepszy od std::make_tuple(&i);?
Laurynas Lazauskas
6
@LaurynasLazauskas Jest inaczej. Ten ostatni, który pokazałeś, zapisuje wskaźnik do i, a nie odniesienie do niego.
Columbo
Hm ... Myślę, że nadal nie mogę odróżnić tych dwóch tak dobrze, jak bym chciał ... Cóż, dziękuję.
Laurynas Lazauskas
@Columbo W jaki sposób możliwa jest tablica opakowań referencyjnych, jeśli nie mają one stanu zerowego? Czy tablice zwykle nie zaczynają się od wszystkich elementów ustawionych na stan null?
anatolyg
2
@anatolyg Co przeszkadza Ci w zainicjowaniu tej tablicy?
Columbo
27

Istnieją co najmniej dwa motywujące cele std::reference_wrapper<T>:

  1. Ma na celu nadanie semantyki referencyjnej obiektom przekazywanym jako parametr wartości do szablonów funkcji. Na przykład, możesz mieć duży obiekt funkcji, który chcesz przekazać, std::for_each()który przyjmuje parametr obiektu funkcji według wartości. Aby uniknąć kopiowania obiektu, możesz użyć

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

    Przekazywanie argumentów jak std::reference_wrapper<T>do std::bind()wyrażenia jest dość powszechne, aby związać argumentów przez odniesienie zamiast przez wartość.

  2. Kiedy używasz an std::reference_wrapper<T>z std::make_tuple()odpowiednim elementem krotki staje się T&a zamiast a T:

    T object;
    f(std::make_tuple(1, std::ref(object)));
Dietmar Kühl
źródło
Czy możesz podać przykład kodu dla pierwszego przypadku?
user1708860
1
@ user1708860: masz na myśli inny niż ten podany ...?
Dietmar Kühl
Mam na myśli rzeczywisty kod, który pasuje do std :: ref (fun), ponieważ nie rozumiem, jak jest używany (chyba że fun jest obiektem, a nie funkcją ...)
user1708860
2
@ user1708860: tak, najprawdopodobniej funjest to obiekt funkcji (tj. obiekt klasy z operatorem wywołania funkcji), a nie funkcja: jeśli okaże funsię, że jest to rzeczywista funkcja, std::ref(fun)nie ma celu i powoduje, że kod jest potencjalnie wolniejszy.
Dietmar Kühl
23

Inną różnicą, jeśli chodzi o reference_wrappersamodokumentujący się kod, jest to, że użycie elementu zasadniczo zrzeka się własności obiektu. W przeciwieństwie do tego, unique_ptrstwierdza własność, podczas gdy czysty wskaźnik może być własnością lub nie (nie można tego wiedzieć bez spojrzenia na wiele powiązanego kodu):

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
źródło
3
O ile nie jest to kod sprzed wersji C ++ 11, pierwszy przykład powinien sugerować opcjonalne wartości, których nie należy do właściciela, na przykład w przypadku wyszukiwania w pamięci podręcznej na podstawie indeksu. Byłoby miło, gdyby std dostarczyło nam coś standardowego do reprezentowania niezerowej wartości posiadanej (unikalne i wspólne warianty)
Bwmat
Być może nie jest to tak ważne w C ++ 11, gdzie zwykłe wskaźniki i tak prawie zawsze będą zapożyczonymi wartościami.
Elling
reference_wrapperjest lepszy od surowych wskaźników nie tylko dlatego, że jest oczywiste, że nie jest właścicielem, ale także dlatego, że nie może być nullptr(bez oszustw), a zatem użytkownicy wiedzą, że nie mogą przejść nullptr(bez oszustw) i wiesz, że nie musisz sprawdź to.
underscore_d
19

Można o tym myśleć jako o wygodnej opasce wokół odniesień, dzięki czemu można ich używać w kontenerach.

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

W zasadzie jest to CopyAssignablewersja T&. Zawsze, gdy potrzebujesz odniesienia, ale musi być przypisane, użyj std::reference_wrapper<T>lub jego funkcji pomocniczej std::ref(). Lub użyj wskaźnika.


Inne dziwactwa 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

I porównanie:

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
źródło
1
@LaurynasLazauskas Można by bezpośrednio wywołać obiekty funkcyjne zawarte w opakowaniu. Jest to również wyjaśnione w mojej odpowiedzi.
Columbo
2
Ponieważ implementacja referencyjna jest tylko wskaźnikiem wewnątrz, nie mogę zrozumieć, dlaczego otoki dodają jakiekolwiek pośrednie lub obniżenie wydajności
Ryga
4
Nie powinno to być bardziej pośrednie niż proste odniesienie, jeśli chodzi o kod wydania
Ryga
3
Spodziewałbym się, że kompilator wbuduje trywialny reference_wrapperkod, czyniąc go identycznym z kodem, który używa wskaźnika lub referencji.
David Stone
4
@LaurynasLazauskas: std::reference_wrapperma gwarancję, że obiekt nigdy nie jest pusty. Rozważmy członka klasy std::vector<T *>. Musisz zbadać cały kod klasy, aby zobaczyć, czy ten obiekt może kiedykolwiek przechowywać a nullptrw wektorze, podczas gdy z std::reference_wrapper<T>gwarancją, że masz prawidłowe obiekty.
David Stone