Почнемо розрізняти спостереження за елементами в контейнері та змінювати їх на місці.
Спостереження за елементами
Розглянемо простий приклад:
vector<int> v = {1, 3, 5, 7, 9};
for (auto x : v)
cout << x << ' ';
Наведений вище код друкує елементи ( int
и) у vector
:
1 3 5 7 9
Тепер розглянемо інший випадок, у якому векторні елементи - це не просто прості цілі числа, а екземпляри складнішого класу, із спеціальним конструктором копіювання тощо.
// 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;
}
Якщо ми використовуємо наведений вище for (auto x : v) {...}
синтаксис із цим новим класом:
vector<X> v = {1, 3, 5, 7, 9};
cout << "\nElements:\n";
for (auto x : v)
{
cout << x << ' ';
}
вихід є чимось на кшталт:
[... 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
Як це можна прочитати з результату, виклики конструктора копіювання здійснюються під час ітерацій циклу на основі діапазону.
Це тому, що ми захоплюємо елементи з контейнера за значенням
( auto x
частина в for (auto x : v)
).
Це неефективний код, наприклад, якщо ці елементи є екземплярами std::string
, можна виділити купу пам'яті, за допомогою дорогих поїздок до менеджера пам'яті тощо. Це марно, якщо ми просто хочемо спостерігати за елементами в контейнері.
Отже, є кращий синтаксис: захоплення за const
посиланням , тобто const auto&
:
vector<X> v = {1, 3, 5, 7, 9};
cout << "\nElements:\n";
for (const auto& x : v)
{
cout << x << ' ';
}
Тепер вихід:
[... copy constructor calls for vector<X> initialization ...]
Elements:
1 3 5 7 9
Без помилкового (і потенційно дорогого) виклику конструктора копії.
Таким чином, при спостереженні елементів в контейнері (наприклад, для доступу тільки для читання), наступний синтаксис чудово підходить для простих дешеві-к-копії типів, як int
, double
і т.д.:
for (auto elem : container)
В іншому випадку, захоплення const
посиланням краще в загальному випадку , щоб уникнути марних (і потенційно дорогих) викликів конструктора копій:
for (const auto& elem : container)
Модифікація елементів у контейнері
Якщо ми хочемо змінити елементи в контейнері за допомогою діапазону for
, наведені вище for (auto elem : container)
та for (const auto& elem : container)
синтаксиси помилкові.
Насправді в першому випадку elem
зберігається копія оригінального елемента, тому зміни, зроблені на ньому, просто втрачаються і не зберігаються постійно в контейнері, наприклад:
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 << ' ';
Вихід - це лише початкова послідовність:
1 3 5 7 9
Натомість спроба використання for (const auto& x : v)
просто не вдається зібрати.
g ++ видає повідомлення про помилку приблизно так:
TestRangeFor.cpp:138:11: error: assignment of read-only reference 'x'
x *= 10;
^
Правильний підхід у цьому випадку відображає за допомогою не- const
посилання:
vector<int> v = {1, 3, 5, 7, 9};
for (auto& x : v)
x *= 10;
for (auto x : v)
cout << x << ' ';
Вихід (як очікувалося):
10 30 50 70 90
Цей for (auto& elem : container)
синтаксис працює також для більш складних типів, наприклад, враховуючи 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 << ' ';
вихід:
Hi Bob! Hi Jeff! Hi Connie!
Особливий випадок проксі-ітераторів
Припустимо, у нас є vector<bool>
, і ми хочемо перетворити логічний булевий стан його елементів, використовуючи вищевказаний синтаксис:
vector<bool> v = {true, false, false, true};
for (auto& x : v)
x = !x;
Вищевказаний код не може скластись.
g ++ видає повідомлення про помилку, подібне до цього:
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)
^
Проблема полягає в тому, що std::vector
шаблон спеціалізується на bool
, з реалізацією , що пакети з bool
˙s для оптимізації простору (кожен логічне значення зберігається в один біт, вісім «Boolean» біт в байті).
Через це (оскільки повернути посилання на один біт неможливо),
vector<bool>
використовується так званий шаблон "проксі-ітератор" . "Проксі-ітератор" - це ітератор, який при відхиленні не дає звичайного bool &
, а натомість повертає (за значенням) тимчасовий об'єкт , в який може бути перетворений проксі-класbool
. (Дивіться також це питання та відповідні відповіді тут на StackOverflow.)
Для зміни елементів на місці vector<bool>
, слід використовувати новий вид синтаксису (використовуючи auto&&
):
for (auto&& x : v)
x = !x;
Наступний код працює добре:
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 << ' ';
та виходи:
false true true false
Зауважте, що for (auto&& elem : container)
синтаксис працює і в інших випадках звичайних (не проксі) ітераторів (наприклад, для a vector<int>
чи a vector<string>
).
(Як бічне зауваження, вищезгаданий синтаксис "спостереження" for (const auto& elem : container)
працює чудово і для випадку проксі-ітератора.)
Підсумок
Вищенаведене обговорення може бути узагальнено в наступних рекомендаціях:
Для спостереження за елементами використовуйте такий синтаксис:
for (const auto& elem : container) // capture by const reference
Якщо об'єкти копіювати дешево (наприклад, int
s, double
s тощо), можна скористатися дещо спрощеною формою:
for (auto elem : container) // capture by value
Для зміни елементів на місці використовуйте:
for (auto& elem : container) // capture by (non-const) reference
Якщо контейнер використовує "проксі-ітератори" (наприклад std::vector<bool>
), використовуйте:
for (auto&& elem : container) // capture by &&
Звичайно, якщо є необхідність зробити локальну копію елемента всередині корпусу циклу, захоплення значенням ( for (auto elem : container)
) - хороший вибір.
Додаткові примітки до загального коду
У загальному коді , оскільки ми не можемо робити припущення про те, що загальний тип T
є дешевим для копіювання, у режимі спостереження його завжди безпечно використовувати for (const auto& elem : container)
.
(Це не запустить потенційно дорогі непотрібні копії, буде добре працювати також для таких типів, як дешеві для копіювання int
, а також для контейнерів, що використовують проксі-ітератори, наприклад std::vector<bool>
.)
Більше того, у модифікаційному режимі, якщо ми хочемо, щоб загальний код працював і у разі проксі-ітераторів, найкращим варіантом є for (auto&& elem : container)
.
(Це буде добре працювати також для контейнерів, що використовують звичайні непроксі-ітератори, наприклад, std::vector<int>
або std::vector<string>
.)
Отже, в загальному коді можна навести наступні вказівки:
Для спостереження за елементами використовуйте:
for (const auto& elem : container)
Для зміни елементів на місці використовуйте:
for (auto&& elem : container)