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 int
w vector
:
1 3 5 7 9
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;
}
int Get() 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 for vector<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 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&
:
vector<X> v = {1, 3, 5, 7, 9};
cout << "\nElements:\n";
for (const auto& x : v)
{
cout << x << ' ';
}
Teraz dane wyjściowe to:
[... copy constructor calls for vector<X> initialization ...]
Elements:
1 3 5 7 9
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 .:
for (auto elem : container)
W przeciwnym razie przechwytywanie przez const
odniesienie jest lepsze w ogólnym przypadku , aby uniknąć niepotrzebnych (i potencjalnie drogich) wywołań konstruktora kopiowania:
for (const auto& 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 elem
przechowuje 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:
1 3 5 7 9
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 const
odniesienia:
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):
10 30 50 70 90
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 (const auto& x : v)
cout << x << ' ';
dane wyjściowe to:
Hi Bob! Hi Jeff! Hi Connie!
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::vector
szablon wyspecjalizowane dla bool
z implementacji, że pakiety z bool
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ł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 status
for (auto&& x : v) // <-- note use of "auto&&" for proxy iterators
x = !x;
// Print new element values
cout << boolalpha;
for (const auto& x : v)
cout << x << ' ';
i wyniki:
false true true false
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 (const auto& elem : container) // capture by const reference
Jeśli obiekty są tanie do skopiowania (takie jak int
s, double
s 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 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 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:
Do obserwacji elementów użyj:
for (const auto& elem : container)
Aby zmodyfikować elementy na miejscu, użyj:
for (auto&& elem : container)