Каков правильный способ использования С++ 11 на основе диапазона for
?
Какой синтаксис следует использовать? for (auto elem : container)
или for (auto& elem : container)
или for (const auto& elem : container)
?
Или какой-то другой?
Каков правильный способ использования С++ 11 на основе диапазона for
?
Какой синтаксис следует использовать? for (auto elem : container)
или for (auto& elem : container)
или for (const auto& elem : container)
?
Или какой-то другой?
Пусть начнется дифференциация между наблюдением элементов в континенте против их изменения на месте.
Рассмотрим простой пример:
vector<int> v = {1, 3, 5, 7, 9};
for (auto x : v)
cout << x << ' ';
Вышеприведенный код печатает элементы (int
s) в 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
Как он может быть прочитан на выходе, вызовы copy constructor выполняются во время
диапазон для итераций цикла.
Это происходит потому, что мы захватываем элементы из контейнера по значению
(часть 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!
Предположим, что a 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
для оптимизации пространства (каждое логическое значение
хранится в одном бите, восемь "булевых" битов в байте).
Из-за этого (поскольку невозможно вернуть ссылку на один бит)
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>
или 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)
Нет правильного способа использования for (auto elem : container)
, или for (auto& elem : container)
или for (const auto& elem : container)
. Вы просто выражаете то, что хотите.
Позвольте мне подробнее остановиться на этом. Позвольте прогуляться.
for (auto elem : container) ...
Это синтаксический сахар для:
for(auto it = container.begin(); it != container.end(); ++it) {
// Observe that this is a copy by value.
auto elem = *it;
}
Вы можете использовать этот, если в вашем контейнере содержатся элементы, которые дешево копировать.
for (auto& elem : container) ...
Это синтаксический сахар для:
for(auto it = container.begin(); it != container.end(); ++it) {
// Now you're directly modifying the elements
// because elem is an lvalue reference
auto& elem = *it;
}
Используйте это, если вы хотите, например, писать элементы в контейнере.
for (const auto& elem : container) ...
Это синтаксический сахар для:
for(auto it = container.begin(); it != container.end(); ++it) {
// You just want to read stuff, no modification
const auto& elem = *it;
}
Как говорится в комментарии, просто для чтения. И что об этом, все правильно "правильно" при правильном использовании.
Правильное средство всегда
for(auto&& elem : container)
Это гарантирует сохранение всей семантики.
В то время как первоначальная мотивация цикла range-for могла быть легкостью итерации по элементам контейнера, синтаксис достаточно общий, чтобы быть полезным даже для объектов, которые не являются чисто контейнерами.
Синтаксическое требование для цикла for заключается в том, что range_expression
поддерживают begin()
и end()
как функции - либо как функции-члены того типа, который он оценивает, либо как функции, не являющиеся членами, что принимает экземпляр типа.
В качестве надуманного примера можно создать диапазон чисел и выполнить итерацию по диапазону, используя следующий класс.
struct Range
{
struct Iterator
{
Iterator(int v, int s) : val(v), step(s) {}
int operator*() const
{
return val;
}
Iterator& operator++()
{
val += step;
return *this;
}
bool operator!=(Iterator const& rhs) const
{
return (this->val < rhs.val);
}
int val;
int step;
};
Range(int l, int h, int s=1) : low(l), high(h), step(s) {}
Iterator begin() const
{
return Iterator(low, step);
}
Iterator end() const
{
return Iterator(high, 1);
}
int low, high, step;
};
Со следующей функцией main
,
#include <iostream>
int main()
{
Range r1(1, 10);
for ( auto item : r1 )
{
std::cout << item << " ";
}
std::cout << std::endl;
Range r2(1, 20, 2);
for ( auto item : r2 )
{
std::cout << item << " ";
}
std::cout << std::endl;
Range r3(1, 20, 3);
for ( auto item : r3 )
{
std::cout << item << " ";
}
std::cout << std::endl;
}
можно получить следующий вывод.
1 2 3 4 5 6 7 8 9
1 3 5 7 9 11 13 15 17 19
1 4 7 10 13 16 19