Co mówi nam auto &&?

168

Jeśli czytasz kod, taki jak

auto&& var = foo();

gdzie foojest dowolna funkcja zwracająca wartość typu T. Następnie varjest lwartość typu rwartość odniesienia do T. Ale co to oznacza var? Czy to oznacza, że ​​wolno nam kraść zasoby var? Czy są jakieś rozsądne sytuacje, w których powinieneś użyć, auto&&aby powiedzieć czytelnikowi o swoim kodzie, podobnie jak robisz, gdy zwracasz a, unique_ptr<>aby powiedzieć, że masz wyłączną własność? A co, na przykład, T&&kiedy Tjest klasowy?

Chcę tylko zrozumieć, czy istnieją inne przypadki użycia auto&&niż te w programowaniu szablonów; podobnie jak te omówione w przykładach w tym artykule Universal References autorstwa Scotta Meyersa.

MWid
źródło
1
Zastanawiałem się nad tym samym. Rozumiem, jak działa odliczanie typów, ale co mówi mój kod , gdy używam auto&&? Zastanawiałem się nad tym, dlaczego pętla for oparta na zakresie rozszerza się, aby użyć jej auto&&jako przykładu, ale nie doszedłem do tego. Być może ktokolwiek odpowie, może to wyjaśnić.
Joseph Mansfield
1
Czy to w ogóle legalne? Chodzi mi o to, że moment T jest niszczony natychmiast po foopowrocie, przechowywanie rvalue ref to brzmi jak UB do ne.
1
@aleguna To jest całkowicie legalne. Nie chcę zwracać odwołania lub wskaźnika do zmiennej lokalnej, ale wartość. Funkcja foomoże być na przykład wyglądać tak: int foo(){return 1;}.
MWid
9
@aleguna odwołania do tymczasowych wydłużają okres istnienia, tak jak w C ++ 98.
ecatmur
5
Rozszerzenie okresu istnienia @aleguna działa tylko z lokalnymi tymczasowymi, a nie z funkcjami zwracającymi odwołania. Zobacz stackoverflow.com/a/2784304/567292
ecatmur

Odpowiedzi:

232

Używając auto&& var = <initializer>you, mówisz: zaakceptuję dowolny inicjalizator, niezależnie od tego, czy jest to wyrażenie l-wartość, czy r-wartość i zachowam jego stałość . Jest to zwykle używane do przekazywania (zwykle z T&&). Powodem, dla którego to działa, jest to, że „uniwersalne odniesienie” auto&&lub T&&będzie wiązać się z czymkolwiek .

Możesz powiedzieć, dlaczego nie użyć po prostu znaku, const auto&ponieważ to również będzie wiązać się z czymkolwiek? Problem z używaniem constreferencji polega na tym, że tak jest const! Nie będzie można później powiązać go z żadnymi odwołaniami niebędącymi stałymi ani wywoływać funkcji składowych, które nie są oznaczone const.

Jako przykład wyobraź sobie, że chcesz uzyskać a std::vector, przenieś iterator do jego pierwszego elementu i zmodyfikuj w jakiś sposób wartość wskazywaną przez ten iterator:

auto&& vec = some_expression_that_may_be_rvalue_or_lvalue;
auto i = std::begin(vec);
(*i)++;

Ten kod skompiluje się dobrze, niezależnie od wyrażenia inicjatora. Alternatywy, które mogą auto&&zawieść w następujący sposób:

auto         => will copy the vector, but we wanted a reference
auto&        => will only bind to modifiable lvalues
const auto&  => will bind to anything but make it const, giving us const_iterator
const auto&& => will bind only to rvalues

Więc do tego auto&&działa idealnie! Przykładem takiego użycia auto&&jest forpętla oparta na zakresie . Zobacz moje drugie pytanie, aby uzyskać więcej informacji.

Jeśli następnie użyjesz std::forwardw swoim auto&&odwołaniu, aby zachować fakt, że pierwotnie była to lwartość lub rwartość, twój kod mówi: Teraz, gdy mam twój obiekt z wyrażenia lwartość lub rwartość, chcę zachować każdą pierwotną wartość tak, żebym mógł z niego korzystać najefektywniej - może to go unieważnić. Jak w:

auto&& var = some_expression_that_may_be_rvalue_or_lvalue;
// var was initialized with either an lvalue or rvalue, but var itself
// is an lvalue because named rvalues are lvalues
use_it_elsewhere(std::forward<decltype(var)>(var));

Pozwala use_it_elsewhereto wyrwać jego wnętrzności ze względu na wydajność (unikanie kopii), gdy oryginalny inicjator był modyfikowalną rvalue.

Co to oznacza, jeśli chodzi o to, czy możemy, lub kiedy możemy kraść zasoby var? Cóż, ponieważ auto&&wola wiąże się z czymkolwiek, nie możemy sami próbować wyrwać varwnętrzności - może to być wartość l, a nawet stała. Możemy jednak korzystać std::forwardz innych funkcji, które mogą całkowicie spustoszyć jego wnętrze. Gdy tylko to zrobimy, powinniśmy uważać, że jesteśmy varw stanie nieważności.

Teraz zastosujmy to do przypadku auto&& var = foo();, jak podano w pytaniu, gdzie foo zwraca Twartość a. W takim przypadku wiemy na pewno, że typ varzostanie wydedukowany jako T&&. Ponieważ wiemy na pewno, że jest to wartość r, nie potrzebujemy std::forwardpozwolenia na kradzież jej zasobów. W tym konkretnym przypadku, wiedząc, że foozwraca wartość , czytelnik powinien po prostu odczytać to jako: Biorę odwołanie do wartości r do tymczasowego zwróconego z foo, więc mogę z radością odejść od tego.


Jako uzupełnienie, myślę, że warto wspomnieć, kiedy some_expression_that_may_be_rvalue_or_lvaluemoże pojawić się wyrażenie takie jak , inne niż sytuacja, w której kod może się zmienić. Oto wymyślony przykład:

std::vector<int> global_vec{1, 2, 3, 4};

template <typename T>
T get_vector()
{
  return global_vec;
}

template <typename T>
void foo()
{
  auto&& vec = get_vector<T>();
  auto i = std::begin(vec);
  (*i)++;
  std::cout << vec[0] << std::endl;
}

Oto get_vector<T>()cudowne wyrażenie, które może być lwartością lub rwartością w zależności od typu ogólnego T. Zasadniczo zmieniamy typ zwracania get_vectorprzez parametr szablonu foo.

Kiedy wywołujemy foo<std::vector<int>>, get_vectorzwróci global_vecwartość, co daje wyrażenie rvalue. Alternatywnie, gdy wywołasz foo<std::vector<int>&>, get_vectorzwróci global_vecprzez odniesienie, co spowoduje wyrażenie lvalue.

Jeśli zrobimy:

foo<std::vector<int>>();
std::cout << global_vec[0] << std::endl;
foo<std::vector<int>&>();
std::cout << global_vec[0] << std::endl;

Zgodnie z oczekiwaniami otrzymujemy następujący wynik:

2
1
2
2

Jeśli było zmienić auto&&w kodzie, aby którykolwiek z auto, auto&, const auto&czy const auto&&wtedy nie będzie uzyskać wynik chcemy.


Alternatywnym sposobem zmiany logiki programu w zależności od tego, czy auto&&odwołanie jest zainicjowane wyrażeniem l-wartość, czy r-wartość, jest użycie cech typu:

if (std::is_lvalue_reference<decltype(var)>::value) {
  // var was initialised with an lvalue expression
} else if (std::is_rvalue_reference<decltype(var)>::value) {
  // var was initialised with an rvalue expression
}
Joseph Mansfield
źródło
2
Nie możemy po prostu powiedzieć, że T vec = get_vector<T>();funkcja foo? A może upraszczam to do absurdu :)
Asterisk
@Asterisk No bcoz T vec może być przypisane do lvalue tylko w przypadku std :: vector <int &>, a jeśli T jest std :: vector <int>, wtedy użyjemy wywołania według wartości, które jest nieefektywne
Kapil
1
auto & daje ten sam wynik. Używam MSVC 2015. A GCC generuje błąd.
Sergey Podobry
Tutaj używam MSVC 2015, auto & daje takie same wyniki jak auto &&.
Kehe CAI
Dlaczego int i; auto && j = i; dozwolone, ale int i; int && j = i; nie jest ?
SeventhSon84
14

Po pierwsze, polecam przeczytanie mojej odpowiedzi jako dodatkowej lektury, aby wyjaśnić krok po kroku, jak działa dedukcja argumentów szablonowych dla odwołań uniwersalnych.

Czy to oznacza, że ​​wolno nam kraść zasoby var?

Niekoniecznie. Co się stanie, jeśli foo()nagle zwróciłeś odniesienie lub zmieniłeś połączenie, ale zapomniałeś zaktualizować użycie var? Lub jeśli jesteś w kodzie ogólnym i zwracany typ foo()może się zmieniać w zależności od parametrów?

Pomyśl o tym, auto&&że jest dokładnie tym samym, co T&&in template<class T> void f(T&& v);, ponieważ to (prawie ) dokładnie to. Co robisz z odwołaniami uniwersalnymi w funkcjach, kiedy musisz je przekazać lub w jakikolwiek sposób wykorzystać? Używasz, std::forward<T>(v)aby odzyskać pierwotną kategorię wartości. Jeśli przed przekazaniem do funkcji była to lwartość, po przekazaniu pozostaje lwartością std::forward. Jeśli była to wartość r, stanie się ponownie wartością r (pamiętaj, że nazwane odniesienie do rwartości jest lwartością).

Jak więc varprawidłowo używać w ogólny sposób? Użyj std::forward<decltype(var)>(var). Będzie to działać dokładnie tak samo, jak std::forward<T>(v)w powyższym szablonie funkcji. Jeśli varjest a T&&, otrzymasz z powrotem wartość r, a jeśli tak T&, otrzymasz z powrotem wartość l.

A więc wracając do tematu: Co nam powiedz auto&& v = f();i std::forward<decltype(v)>(v)w bazie kodu? Mówią nam, że vzostaną pozyskane i przekazane w najbardziej efektywny sposób. Pamiętaj jednak, że po przesłaniu takiej zmiennej jest możliwe, że jest ona przeniesiona, więc byłoby niepoprawne, użyj jej dalej bez resetowania.

Osobiście używam auto&&w kodzie ogólnym, gdy potrzebuję zmiennej, którą można modyfikować . Doskonałe przekazywanie wartości r jest modyfikacją, ponieważ operacja przenoszenia potencjalnie kradnie jej wnętrzności. Jeśli chcę być po prostu leniwy (tj. Nie przeliterować nazwy typu, nawet jeśli ją znam) i nie muszę modyfikować (np. Podczas drukowania elementów zakresu), będę się trzymać auto const&.


autojest tak daleko różne, że auto v = {1,2,3};uczyni , podczas gdy będzie awaria odliczenie.vstd::initializer_listf({1,2,3})

Xeo
źródło
W pierwszej części twojej odpowiedzi mam na myśli, że jeśli foo()zwraca typ wartości T, to var(to wyrażenie) będzie lwartością, a jego typ (tego wyrażenia) będzie odniesieniem do rwartości T(tj T&&.).
MWid
@MWid: Ma sens, usunąłem pierwszą część.
Xeo,
3

Rozważmy jakiś typ, Tktóry ma konstruktora przenoszenia i załóżmy

T t( foo() );

używa tego konstruktora przenoszenia.

Teraz użyjmy odwołania pośredniego, aby przechwycić wynik z foo:

auto const &ref = foo();

wyklucza to użycie konstruktora move, więc wartość zwracana będzie musiała zostać skopiowana zamiast przeniesiona (nawet jeśli użyjemy std::movetutaj, nie możemy faktycznie przejść przez const ref)

T t(std::move(ref));   // invokes T::T(T const&)

Jeśli jednak używamy

auto &&rvref = foo();
// ...
T t(std::move(rvref)); // invokes T::T(T &&)

konstruktor przenoszenia jest nadal dostępny.


Aby odpowiedzieć na inne pytania:

... Czy są jakieś rozsądne sytuacje, w których powinieneś użyć auto &&, aby powiedzieć coś czytelnikowi o swoim kodzie ...

Pierwszą rzeczą, jak mówi Xeo, jest zasadniczo przekazanie X tak wydajnie, jak to możliwe , niezależnie od typu X. Tak więc, widząc kod, który używa auto&&wewnętrznie, powinno komunikować, że użyje wewnętrznie semantyki przenoszenia tam, gdzie jest to właściwe.

... tak jak wtedy, gdy zwracasz unique_ptr <>, aby powiedzieć, że masz wyłączne prawo własności ...

Kiedy szablon funkcji przyjmuje argument typu T&&, to mówi, że może przesunąć przekazany obiekt. Zwrócenie unique_ptrjawnie daje własność wywołującemu; przyjmowanie T&&może usunąć własności z osoby dzwoniącej (jeżeli konstruktor ruch istnieje, itd.).

Bezużyteczny
źródło
2
Nie jestem pewien, czy twój drugi przykład jest prawidłowy. Czy nie potrzebujesz idealnego przekazywania, aby wywołać konstruktor przenoszenia?
3
To jest źle. W obu przypadkach konstruktor kopia jest nazywana, ponieważ refi rvrefto zarówno lwartościami. Jeśli chcesz konstruktora przenoszenia, musisz napisać T t(std::move(rvref)).
MWid
Czy miałeś na myśli const ref w swoim pierwszym przykładzie auto const &:?
PiotrNycz
@aleguna - ty i MWid macie rację, dzięki. Poprawiłem odpowiedź.
Bezużyteczne
1
@Useless Masz rację. Ale to nie odpowiada na moje pytanie. Kiedy używasz auto&&i co powiesz czytelnikowi swojego kodu używając auto&&?
MWid
-3

auto &&Składnia wykorzystuje dwie nowe funkcje C ++ 11:

  1. autoCzęść kompilator pozwala wydedukować typu na podstawie kontekstu (wartość zwracana w tym przypadku). To jest bez żadnych kwalifikacji referencyjnych (pozwalających określić, czy chcesz T, T &czy T &&dla typu wywnioskowanego T).

  2. To &&jest nowa semantyka przenoszenia. Typ obsługujący semantykę przenoszenia implementuje konstruktora, T(T && other)który optymalnie przenosi zawartość w nowym typie. Pozwala to obiektowi na zamianę reprezentacji wewnętrznej zamiast wykonywania głębokiej kopii.

Dzięki temu możesz mieć coś takiego:

std::vector<std::string> foo();

Więc:

auto var = foo();

wykona kopię zwracanego wektora (drogiego), ale:

auto &&var = foo();

zamieni wewnętrzną reprezentację wektora (wektor z fooi pusty wektor z var), więc będzie szybszy.

Jest to używane w nowej składni pętli for:

for (auto &item : foo())
    std::cout << item << std::endl;

W przypadku, gdy dla pętli trzyma auto &&wartości powrotnej z fooi itemjest punktem odniesienia dla każdej wartości foo.

reece
źródło
To jest niepoprawne. auto&&niczego nie ruszy, po prostu utworzy odniesienie. To, czy jest to odniesienie l-wartość, czy r-wartość, zależy od wyrażenia użytego do jego zainicjowania.
Joseph Mansfield,
W obu przypadkach zostanie wywołany konstruktor ruchu, ponieważ std::vectori std::stringsą możliwe do zbudowania ruchu. Nie ma to nic wspólnego z typem var.
MWid
1
@MWid: Właściwie, wywołanie konstruktora kopiowania / przenoszenia można również całkowicie wyeliminować za pomocą RVO.
Matthieu M.
@MatthieuM. Masz rację. Ale myślę, że w powyższym przykładzie konstruktor kopii nigdy nie zostanie wywołany, ponieważ wszystko jest możliwe do zbudowania za pomocą ruchu.
MWid
1
@MWid: Chodziło mi o to, że nawet konstruktor przenoszenia można wyeliminować. Elision przebija ruch (jest tańszy).
Matthieu M.,