W rzeczywistości ma to niewiele wspólnego z opcją opartą na zakresie. To samo można powiedzieć o każdym auto (const)(&) x = <expr>;.
Matthieu M.
2
@MatthieuM: Ma to oczywiście wiele wspólnego z zasięgiem! Rozważ początkującego, który widzi kilka składni i nie może wybrać, której formy użyć. Celem „pytań i odpowiedzi” była próba wyjaśnienia różnic między niektórymi przypadkami (i omówienia spraw, które dobrze się kompilują, ale są trochę nieefektywne z powodu bezużytecznych głębokich kopii itp.).
Mr.C64
2
@ Mr. C64: Moim zdaniem ma to więcej wspólnego z auto, ogólnie, niż z zasięgiem; bez problemu możesz używać opartego na zasięgu auto! for (int i: v) {}jest całkowicie w porządku. Oczywiście większość punktów, które podniosłeś w odpowiedzi, może mieć więcej wspólnego z typem niż z auto... ale z pytania nie jest jasne, gdzie jest punkt bólu. Osobiście walczyłbym o usunięcie autoz pytania; lub może wyraźnie powiedzieć, że niezależnie od tego, czy używasz, czy autoteż wyraźnie określasz typ, pytanie koncentruje się na wartości / referencji.
Matthieu M.
1
@ MatthieuM .: Jestem otwarty na zmianę tytułu lub edycję pytania w jakiejś formie, która może uczynić je bardziej zrozumiałymi ... Ponownie skupiłem się na omówieniu kilku opcji składni opartych na zakresie (pokazując kod, który się kompiluje, ale jest nieefektywny, kod, który się nie kompiluje itp.) i próba zaoferowania komuś wskazówek (szczególnie na poziomie początkującym) zbliżających się do pętli w zakresie C ++ 11.
Mr.C64
Odpowiedzi:
389
Zacznijmy od rozróżnienia między obserwowaniem elementów w pojemniku a modyfikowaniem ich na miejscu.
Obserwować elementy
Rozważmy prosty przykład:
vector<int> v ={1,3,5,7,9};for(auto x : v)
cout << x <<' ';
Powyższy kod drukuje elementy intw vector:
13579
Rozważmy teraz inny przypadek, w którym elementy wektorowe nie są zwykłymi liczbami całkowitymi, ale instancjami bardziej złożonej klasy z niestandardowym konstruktorem kopii itp.
// A sample test class, with custom copy semantics.class X
{public:
X(): m_data(0){}
X(int data): m_data(data){}~X(){}
X(const X& other): m_data(other.m_data){ cout <<"X copy ctor.\n";}
X&operator=(const X& other){
m_data = other.m_data;
cout <<"X copy assign.\n";return*this;}intGet()const{return m_data;}private:int m_data;};
ostream&operator<<(ostream& os,const X& x){
os << x.Get();return os;}
Jeśli użyjemy powyższej for (auto x : v) {...}składni z nową klasą:
vector<X> v ={1,3,5,7,9};
cout <<"\nElements:\n";for(auto x : v){
cout << x <<' ';}
wynik jest podobny do:
[... copy constructor calls forvector<X> initialization ...]Elements:
X copy ctor.1 X copy ctor.3 X copy ctor.5 X copy ctor.7 X copy ctor.9
Jak można odczytać z wyjścia, wywołania konstruktora kopiowania są wykonywane podczas iteracji pętli na podstawie zakresu.
Wynika to z tego, że przechwytujemy elementy z kontenera według wartości
( auto xczęść wfor (auto x : v) ).
Jest to nieefektywny kod, np. Jeśli te elementy są instancjami std::string, można dokonać alokacji pamięci sterty, kosztownych podróży do menedżera pamięci itp. Jest to bezużyteczne, jeśli chcemy tylko obserwować elementy w kontenerze.
Dostępna jest więc lepsza składnia: przechwytywanie przez constreferencję , tj . const auto&:
vector<X> v ={1,3,5,7,9};
cout <<"\nElements:\n";for(constauto& x : v){
cout << x <<' ';}
Bez żadnego fałszywego (i potencjalnie kosztownego) wywołania konstruktora kopiowania.
Tak więc, gdy obserwując elementy w pojemniku (czyli dla dostępem tylko do odczytu), następujące składnia jest w porządku dla prostych tani do imitowania typów, jak int, doubleitp .:
for(auto elem : container)
W przeciwnym razie przechwytywanie przez constodniesienie jest lepsze w ogólnym przypadku , aby uniknąć niepotrzebnych (i potencjalnie drogich) wywołań konstruktora kopiowania:
for(constauto& elem : container)
Modyfikacja elementów w kontenerze
Jeśli chcemy zmodyfikować elementy w kontenerze za pomocą zakresu for, powyższe for (auto elem : container)i for (const auto& elem : container)
składnie są niepoprawne.
W rzeczywistości w pierwszym przypadku elemprzechowuje kopię oryginalnego elementu, więc modyfikacje dokonane w nim są po prostu tracone i nie są trwale przechowywane w kontenerze, np .:
vector<int> v ={1,3,5,7,9};for(auto x : v)// <-- capture by value (copy)
x *=10;// <-- a local temporary copy ("x") is modified,// *not* the original vector element.for(auto x : v)
cout << x <<' ';
Dane wyjściowe to tylko sekwencja początkowa:
13579
Zamiast tego próba użycia for (const auto& x : v)po prostu się nie kompiluje.
g ++ wyświetla komunikat o błędzie podobny do tego:
TestRangeFor.cpp:138:11: error: assignment of read-only reference 'x'
x *=10;^
Prawidłowe podejście w tym przypadku polega na uchwyceniu przez brak constodniesienia:
vector<int> v ={1,3,5,7,9};for(auto& x : v)
x *=10;for(auto x : v)
cout << x <<' ';
Dane wyjściowe są (zgodnie z oczekiwaniami):
1030507090
Ta for (auto& elem : container)składnia działa również w przypadku bardziej złożonych typów, np. Biorąc pod uwagę vector<string>:
vector<string> v ={"Bob","Jeff","Connie"};// Modify elements in place: use "auto &"for(auto& x : v)
x ="Hi "+ x +"!";// Output elements (*observing* --> use "const auto&")for(constauto& x : v)
cout << x <<' ';
dane wyjściowe to:
HiBob!HiJeff!HiConnie!
Szczególny przypadek iteratorów proxy
Załóżmy, że mamy vector<bool>, i chcemy odwrócić logiczny stan logiczny jego elementów, używając powyższej składni:
vector<bool> v ={true,false,false,true};for(auto& x : v)
x =!x;
Powyższy kod się nie kompiluje.
g ++ wyświetla komunikat o błędzie podobny do tego:
TestRangeFor.cpp:168:20: error: invalid initialization of non-const reference of
type 'std::_Bit_reference&' from an rvalue of type 'std::_Bit_iterator::referen
ce {aka std::_Bit_reference}'for(auto& x : v)^
Problemem jest to, że std::vectorszablon wyspecjalizowane dla boolz implementacji, że pakiety z boolS do miejsca Optymalizacja (każda wartość logiczna jest przechowywany w jednym kawałku, ośmiu „wartość logiczna” bitów bajtu).
Z tego powodu (ponieważ nie można zwrócić referencji do jednego bitu)
vector<bool>używa tak zwanego wzorca „iteratora proxy” . „Iterator proxy” to iterator, który po dereferencji nie daje zwykłego bool &, ale zwraca (według wartości) obiekt tymczasowy , do którego można przekształcić klasę proxybool . (Zobacz także to pytanie i powiązane odpowiedzi tutaj na StackOverflow.)
Aby zmodyfikować w miejscu elementy vector<bool>, należy użyć nowego rodzaju składni (using auto&&):
for(auto&& x : v)
x =!x;
Poniższy kod działa poprawnie:
vector<bool> v ={true,false,false,true};// Invert boolean statusfor(auto&& x : v)// <-- note use of "auto&&" for proxy iterators
x =!x;// Print new element values
cout << boolalpha;for(constauto& x : v)
cout << x <<' ';
i wyniki:
falsetruetruefalse
Zauważ, że for (auto&& elem : container)składnia działa również w innych przypadkach zwykłych iteratorów (niebędących proxy) (np. Dla a vector<int>lub avector<string> ).
(Na marginesie, wspomniana wyżej „obserwująca” składnia for (const auto& elem : container)działa dobrze również w przypadku iteratora proxy).
Podsumowanie
Powyższą dyskusję można streścić w następujących wytycznych:
Aby obserwować elementy, użyj następującej składni:
for(constauto& elem : container)// capture by const reference
Jeśli obiekty są tanie do skopiowania (takie jak ints, doubles itp.), Można użyć nieco uproszczonej formy:
for(auto elem : container)// capture by value
Aby zmodyfikować elementy na miejscu, użyj:
for(auto& elem : container)// capture by (non-const) reference
Jeśli kontener używa „iteratorów proxy” (jak std::vector<bool>), użyj:
for(auto&& elem : container)// capture by &&
Oczywiście, jeśli istnieje potrzeba wykonania lokalnej kopii elementu w ciele pętli, dobrym rozwiązaniem jest przechwycenie za pomocą value ( for (auto elem : container)).
Dodatkowe uwagi na temat kodu ogólnego
W ogólnym kodzie , ponieważ nie możemy zakładać, że typ ogólny Tjest tani do kopiowania, w trybie obserwacji zawsze można bezpiecznie używać for (const auto& elem : container).
(Nie uruchomi to potencjalnie kosztownych, bezużytecznych kopii, będzie działać dobrze również w przypadku typów tanich do kopiowania, takich jak int, a także w przypadku kontenerów korzystających z iteratorów proxy itp std::vector<bool>.)
Co więcej, w trybie modyfikacji , jeśli chcemy, aby kod ogólny działał również w przypadku iteratorów proxy, najlepszą opcją jest for (auto&& elem : container).
(Będzie to działać dobrze również w przypadku kontenerów używających zwykłych iteratorów innych niż proxy, takich jak std::vector<int>lubstd::vector<string> .)
Zatem w kodzie ogólnym można podać następujące wytyczne:
Dlaczego nie zawsze używać auto&&? Czy istnieje const auto&&?
Martin Ba
1
Chyba brakuje Ci przypadku, w którym potrzebujesz kopii wewnątrz pętli?
juanchopanza
6
„Jeśli użyje pojemnik«iteratory proxy»” - a ty wiesz, że używa „iteratory proxy” (co może nie być w przypadku kodu ogólne). Myślę więc, że najlepsze jest rzeczywiście auto&&, ponieważ obejmuje ono auto&równie dobrze.
Christian Rau,
5
Dziękuję, to było naprawdę świetne „wprowadzenie do kursu awaryjnego” do składni i kilka wskazówek dotyczących zakresu dla programisty C #. +1.
AndrewJacksonZA
17
Nie ma prawidłowego sposobu użycia for (auto elem : container), lub for (auto& elem : container)lub for (const auto& elem : container). Po prostu wyrażasz, co chcesz.
Pozwól mi rozwinąć tę kwestię. Przejdźmy się.
for(auto elem : container)...
Ten jest cukrem syntaktycznym dla:
for(auto it = container.begin(); it != container.end();++it){// Observe that this is a copy by value.auto elem =*it;}
Możesz użyć tego, jeśli twój kontener zawiera elementy, których kopiowanie jest tanie.
for(auto& elem : container)...
Ten jest cukrem syntaktycznym dla:
for(auto it = container.begin(); it != container.end();++it){// Now you're directly modifying the elements// because elem is an lvalue referenceauto& elem =*it;}
Użyj tego, jeśli chcesz na przykład pisać bezpośrednio do elementów w kontenerze.
for(constauto& elem : container)...
Ten jest cukrem syntaktycznym dla:
for(auto it = container.begin(); it != container.end();++it){// You just want to read stuff, no modificationconstauto& elem =*it;}
Jak mówi komentarz, tylko do czytania. I o to chodzi, wszystko jest „prawidłowe”, gdy jest właściwie używane.
Zamierzałem dać pewne wskazówki, z przykładowymi kodami kompilującymi się (ale nieefektywnymi) lub nieudanymi w kompilacji i wyjaśniającymi dlaczego, i próbować zaproponować pewne rozwiązania.
Mr.C64
2
@ Mr.C64 Och, przepraszam - właśnie zauważyłem, że jest to jedno z pytań typu FAQ. Jestem nowy na tej stronie. Przeprosiny! Twoja odpowiedź jest świetna, głosowałem za nią - ale chciałem też przedstawić bardziej zwięzłą wersję dla tych, którzy chcą istoty . Mam nadzieję, że nie przeszkadzam.
1
@ Mr. C64, na czym polega problem z odpowiedzią OP na pytanie? To tylko kolejna, ważna odpowiedź.
mfontanini,
1
@mfontanini: Nie ma absolutnie żadnego problemu, jeśli ktoś opublikuje odpowiedź, nawet lepszą niż moja. Ostatecznym celem jest wniesienie wysokiej jakości wkładu do społeczności (szczególnie dla początkujących, którzy mogą czuć się zagubieni przed różnymi składniami i różnymi opcjami oferowanymi przez C ++).
Ale co jeśli kontener zwróci tylko modyfikowalne odwołania i chcę wyjaśnić, że nie chcę ich modyfikować w pętli? Czy nie powinienem wtedy użyć, auto const &aby wyjaśnić moją intencję?
RedX
@RedX: Co to jest „modyfikowalne odniesienie”?
Wyścigi lekkości na orbicie
2
@RedX: Referencje nigdy constnie są i nigdy nie można ich modyfikować . W każdym razie moja odpowiedź na to pytanie brzmiałaby „ tak” .
Wyścigi lekkości na orbicie
4
Chociaż może to zadziałać, uważam, że jest to kiepska rada w porównaniu z bardziej dopracowanym i przemyślanym podejściem podanym przez doskonałą i kompleksową odpowiedź pana C64 podaną powyżej. C ++ nie jest redukowane do najmniej powszechnego mianownika.
Podczas gdy początkową motywacją dla pętli zasięgu może być łatwość iteracji po elementach kontenera, składnia jest na tyle ogólna, że jest użyteczna nawet dla obiektów, które nie są czysto kontenerami.
Wymaganiem składniowym dla pętli for jest to, że range_expressionobsługuje begin()i end()jako obie funkcje - albo jako funkcje składowe typu, którego ocenia, albo jako funkcje nie będące członkami, które przyjmują instancję typu.
Jako wymyślony przykład można wygenerować zakres liczb i iterować po nim, używając następującej klasy.
auto (const)(&) x = <expr>;
.auto
, ogólnie, niż z zasięgiem; bez problemu możesz używać opartego na zasięguauto
!for (int i: v) {}
jest całkowicie w porządku. Oczywiście większość punktów, które podniosłeś w odpowiedzi, może mieć więcej wspólnego z typem niż zauto
... ale z pytania nie jest jasne, gdzie jest punkt bólu. Osobiście walczyłbym o usunięcieauto
z pytania; lub może wyraźnie powiedzieć, że niezależnie od tego, czy używasz, czyauto
też wyraźnie określasz typ, pytanie koncentruje się na wartości / referencji.Odpowiedzi:
Zacznijmy od rozróżnienia między obserwowaniem elementów w pojemniku a modyfikowaniem ich na miejscu.
Obserwować elementy
Rozważmy prosty przykład:
Powyższy kod drukuje elementy
int
wvector
:Rozważmy teraz inny przypadek, w którym elementy wektorowe nie są zwykłymi liczbami całkowitymi, ale instancjami bardziej złożonej klasy z niestandardowym konstruktorem kopii itp.
Jeśli użyjemy powyższej
for (auto x : v) {...}
składni z nową klasą:wynik jest podobny do:
Jak można odczytać z wyjścia, wywołania konstruktora kopiowania są wykonywane podczas iteracji pętli na podstawie zakresu.
Wynika to z tego, że przechwytujemy elementy z kontenera według wartości (
auto x
część wfor (auto x : v)
).Jest to nieefektywny kod, np. Jeśli te elementy są instancjami
std::string
, można dokonać alokacji pamięci sterty, kosztownych podróży do menedżera pamięci itp. Jest to bezużyteczne, jeśli chcemy tylko obserwować elementy w kontenerze.Dostępna jest więc lepsza składnia: przechwytywanie przez
const
referencję , tj .const auto&
:Teraz dane wyjściowe to:
Bez żadnego fałszywego (i potencjalnie kosztownego) wywołania konstruktora kopiowania.
Tak więc, gdy obserwując elementy w pojemniku (czyli dla dostępem tylko do odczytu), następujące składnia jest w porządku dla prostych tani do imitowania typów, jak
int
,double
itp .:W przeciwnym razie przechwytywanie przez
const
odniesienie jest lepsze w ogólnym przypadku , aby uniknąć niepotrzebnych (i potencjalnie drogich) wywołań konstruktora kopiowania:Modyfikacja elementów w kontenerze
Jeśli chcemy zmodyfikować elementy w kontenerze za pomocą zakresu
for
, powyższefor (auto elem : container)
ifor (const auto& elem : container)
składnie są niepoprawne.W rzeczywistości w pierwszym przypadku
elem
przechowuje kopię oryginalnego elementu, więc modyfikacje dokonane w nim są po prostu tracone i nie są trwale przechowywane w kontenerze, np .:Dane wyjściowe to tylko sekwencja początkowa:
Zamiast tego próba użycia
for (const auto& x : v)
po prostu się nie kompiluje.g ++ wyświetla komunikat o błędzie podobny do tego:
Prawidłowe podejście w tym przypadku polega na uchwyceniu przez brak
const
odniesienia:Dane wyjściowe są (zgodnie z oczekiwaniami):
Ta
for (auto& elem : container)
składnia działa również w przypadku bardziej złożonych typów, np. Biorąc pod uwagęvector<string>
:dane wyjściowe to:
Szczególny przypadek iteratorów proxy
Załóżmy, że mamy
vector<bool>
, i chcemy odwrócić logiczny stan logiczny jego elementów, używając powyższej składni:Powyższy kod się nie kompiluje.
g ++ wyświetla komunikat o błędzie podobny do tego:
Problemem jest to, że
std::vector
szablon wyspecjalizowane dlabool
z implementacji, że pakiety zbool
S do miejsca Optymalizacja (każda wartość logiczna jest przechowywany w jednym kawałku, ośmiu „wartość logiczna” bitów bajtu).Z tego powodu (ponieważ nie można zwrócić referencji do jednego bitu)
vector<bool>
używa tak zwanego wzorca „iteratora proxy” . „Iterator proxy” to iterator, który po dereferencji nie daje zwykłegobool &
, ale zwraca (według wartości) obiekt tymczasowy , do którego można przekształcić klasę proxybool
. (Zobacz także to pytanie i powiązane odpowiedzi tutaj na StackOverflow.)Aby zmodyfikować w miejscu elementy
vector<bool>
, należy użyć nowego rodzaju składni (usingauto&&
):Poniższy kod działa poprawnie:
i wyniki:
Zauważ, że
for (auto&& elem : container)
składnia działa również w innych przypadkach zwykłych iteratorów (niebędących proxy) (np. Dla avector<int>
lub avector<string>
).(Na marginesie, wspomniana wyżej „obserwująca” składnia
for (const auto& elem : container)
działa dobrze również w przypadku iteratora proxy).Podsumowanie
Powyższą dyskusję można streścić w następujących wytycznych:
Aby obserwować elementy, użyj następującej składni:
Jeśli obiekty są tanie do skopiowania (takie jak
int
s,double
s itp.), Można użyć nieco uproszczonej formy:Aby zmodyfikować elementy na miejscu, użyj:
Jeśli kontener używa „iteratorów proxy” (jak
std::vector<bool>
), użyj:Oczywiście, jeśli istnieje potrzeba wykonania lokalnej kopii elementu w ciele pętli, dobrym rozwiązaniem jest przechwycenie za pomocą value (
for (auto elem : container)
).Dodatkowe uwagi na temat kodu ogólnego
W ogólnym kodzie , ponieważ nie możemy zakładać, że typ ogólny
T
jest tani do kopiowania, w trybie obserwacji zawsze można bezpiecznie używaćfor (const auto& elem : container)
.(Nie uruchomi to potencjalnie kosztownych, bezużytecznych kopii, będzie działać dobrze również w przypadku typów tanich do kopiowania, takich jak
int
, a także w przypadku kontenerów korzystających z iteratorów proxy itpstd::vector<bool>
.)Co więcej, w trybie modyfikacji , jeśli chcemy, aby kod ogólny działał również w przypadku iteratorów proxy, najlepszą opcją jest
for (auto&& elem : container)
.(Będzie to działać dobrze również w przypadku kontenerów używających zwykłych iteratorów innych niż proxy, takich jak
std::vector<int>
lubstd::vector<string>
.)Zatem w kodzie ogólnym można podać następujące wytyczne:
Do obserwacji elementów użyj:
Aby zmodyfikować elementy na miejscu, użyj:
źródło
auto&&
? Czy istniejeconst auto&&
?auto&&
, ponieważ obejmuje onoauto&
równie dobrze.Nie ma prawidłowego sposobu użycia
for (auto elem : container)
, lubfor (auto& elem : container)
lubfor (const auto& elem : container)
. Po prostu wyrażasz, co chcesz.Pozwól mi rozwinąć tę kwestię. Przejdźmy się.
Ten jest cukrem syntaktycznym dla:
Możesz użyć tego, jeśli twój kontener zawiera elementy, których kopiowanie jest tanie.
Ten jest cukrem syntaktycznym dla:
Użyj tego, jeśli chcesz na przykład pisać bezpośrednio do elementów w kontenerze.
Ten jest cukrem syntaktycznym dla:
Jak mówi komentarz, tylko do czytania. I o to chodzi, wszystko jest „prawidłowe”, gdy jest właściwie używane.
źródło
Prawidłowe środki są zawsze
Zagwarantuje to zachowanie całej semantyki.
źródło
auto const &
aby wyjaśnić moją intencję?const
nie są i nigdy nie można ich modyfikować . W każdym razie moja odpowiedź na to pytanie brzmiałaby „ tak” .Podczas gdy początkową motywacją dla pętli zasięgu może być łatwość iteracji po elementach kontenera, składnia jest na tyle ogólna, że jest użyteczna nawet dla obiektów, które nie są czysto kontenerami.
Wymaganiem składniowym dla pętli for jest to, że
range_expression
obsługujebegin()
iend()
jako obie funkcje - albo jako funkcje składowe typu, którego ocenia, albo jako funkcje nie będące członkami, które przyjmują instancję typu.Jako wymyślony przykład można wygenerować zakres liczb i iterować po nim, używając następującej klasy.
Dzięki następującej
main
funkcjiotrzymamy następujące dane wyjściowe.
źródło