Давайте начнем с разграничения между наблюдением за элементами в контейнере и их изменением на месте.
Наблюдая за элементами
Давайте рассмотрим простой пример:
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
Как это можно прочитать из выходных данных, вызовы конструктора копирования выполняются во время итераций цикла на основе диапазона.
Это потому , что мы захватывая элементы из контейнера по значению
(The 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)
синтаксис также работает в других случаях обычных (не-прокси) итераторов (например, для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 &&
Конечно, если необходимо создать локальную копию элемента внутри тела цикла, захват с помощью value ( 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)