Каков правильный способ использования диапазонов в C ++ 11?
212
Как правильно использовать C ++ 11 на основе диапазона for?
Какой синтаксис следует использовать? for (auto elem : container)или for (auto& elem : container)или for (const auto& elem : container)? Или какой-то другой?
На самом деле, это имеет мало общего с диапазоном для. То же самое можно сказать о любом auto (const)(&) x = <expr>;.
Матье М.
2
@MatthieuM: Это имеет много общего с диапазоном на основе для, конечно! Рассмотрим новичка, который видит несколько синтаксисов и не может выбрать, какую форму использовать. Смысл «вопросов и ответов» состоял в том, чтобы попытаться пролить некоторый свет и объяснить различия некоторых случаев (и обсудить случаи, которые хорошо компилируются, но являются неэффективными из-за бесполезных глубоких копий и т. Д.).
Mr.C64
2
@ Mr.C64: Насколько мне известно, это больше связано с autoв целом, чем с диапазоном; Вы можете прекрасно использовать диапазон без каких-либо auto! for (int i: v) {}отлично в порядке. Конечно, большинство вопросов, которые вы поднимаете в своем ответе, может иметь больше общего с типом, чем с auto... но из вопроса неясно, где находится болевая точка. Лично я бы боролся за снятие autoс вопроса; или, может быть, сделать явным, что независимо от того, используете ли вы autoтип или явно называете тип, вопрос сосредоточен на значении / ссылке.
Матье М.
1
@MatthieuM .: Я открыт, чтобы изменить название или отредактировать вопрос в какой-либо форме, которая может сделать их более понятными ... Опять же, я сосредоточился на обсуждении нескольких вариантов синтаксиса на основе диапазона (показ кода, который компилируется, но неэффективный, код, который не компилируется и т. д.) и пытающийся предложить какое-то руководство для кого-то (особенно на начальном уровне), приближающегося к циклам для C ++ 11 на основе диапазона.
Mr.C64
Ответы:
390
Давайте начнем с разграничения между наблюдением за элементами в контейнере и их изменением на месте.
Наблюдая за элементами
Давайте рассмотрим простой пример:
vector<int> v ={1,3,5,7,9};for(auto x : v)
cout << x <<' ';
Приведенный выше код печатает элементы intв vector:
13579
Теперь рассмотрим другой случай, когда векторные элементы представляют собой не просто целые числа, а экземпляры более сложного класса, с пользовательским конструктором копирования и т. Д.
// 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;}
Если мы используем приведенный выше for (auto x : v) {...}синтаксис с этим новым классом:
vector<X> v ={1,3,5,7,9};
cout <<"\nElements:\n";for(auto x : v){
cout << x <<' ';}
вывод что-то вроде:
[... 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
Как это можно прочитать из выходных данных, вызовы конструктора копирования выполняются во время итераций цикла на основе диапазона.
Это потому , что мы захватывая элементы из контейнера по значению
(The auto xчасть в for (auto x : v)).
Это неэффективный код, например, если эти элементы являются экземплярами std::string, выделение памяти в куче может быть выполнено с дорогостоящими поездками в диспетчер памяти и т. Д. Это бесполезно, если мы просто хотим наблюдать за элементами в контейнере.
Таким образом, лучше синтаксис доступен: захват с помощью constссылки , то есть const auto&:
vector<X> v ={1,3,5,7,9};
cout <<"\nElements:\n";for(constauto& x : v){
cout << x <<' ';}
Без какого-либо ложного (и потенциально дорогого) вызова конструктора копирования.
Таким образом, при наблюдении за элементами в контейнере (т. Е. Для доступа только для чтения) следующий синтаксис подходит для простых типов, которые можно копировать дешево , например int, doubleи т. Д .:
for(auto elem : container)
Иначе, захват по constссылке лучше в общем случае , чтобы избежать бесполезных (и потенциально дорогих) вызовов конструктора копирования:
for(constauto& 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 <<' ';
Вывод - это просто начальная последовательность:
13579
Вместо этого попытка использования 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 <<' ';
Выход (как и ожидалось):
1030507090
Этот 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(constauto& x : v)
cout << x <<' ';
выход:
HiBob!HiJeff!HiConnie!
Частный случай прокси-итераторов
Предположим, у нас есть 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 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 <<' ';
и выводы:
falsetruetruefalse
Обратите внимание, что for (auto&& elem : container)синтаксис также работает в других случаях обычных (не-прокси) итераторов (например, дляvector<int> или a vector<string>).
(Как примечание, вышеупомянутый синтаксис "наблюдения" for (const auto& elem : container) отлично работает и для случая с итератором прокси.)
Резюме
Приведенное выше обсуждение может быть кратко изложено в следующих рекомендациях:
Для наблюдения за элементами используйте следующий синтаксис:
for(constauto& elem : container)// capture by const reference
Если объекты дешевы для копирования (например, ints, doubles и т. Д.), Можно использовать слегка упрощенную форму:
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> .)
Итак, в общем коде могут быть предоставлены следующие рекомендации:
Почему не всегда использовать auto&&? Есть ли const auto&&?
Мартин Ба
1
Я полагаю, вы упускаете случай, когда вам действительно нужна копия внутри цикла?
juanchopanza
6
«Если контейнер использует« прокси-итераторы » - и вы знаете, что он использует« прокси-итераторы »(что может быть не так в общем коде). Так что я думаю, что это действительно лучшее auto&&, так как оно auto&одинаково хорошо покрывает .
Кристиан Рау
5
Спасибо, это было действительно отличное «введение в ускоренный курс» к синтаксису и несколько советов по диапазону для программиста C #. +1.
AndrewJacksonZA
17
Там нет правильного способа использовать 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 referenceauto& elem =*it;}
Используйте это, например, для прямой записи в элементы контейнера.
for(constauto& elem : container)...
Это синтаксический сахар для:
for(auto it = container.begin(); it != container.end();++it){// You just want to read stuff, no modificationconstauto& elem =*it;}
Как говорится в комментарии, просто для чтения. И это все, «правильно» при правильном использовании.
Я намеревался дать некоторое руководство, с примерами кодов, компилирующих (но неэффективно), или не в состоянии компилировать, и объясняя почему, и попытаться предложить некоторые решения.
Mr.C64
2
@ Mr.C64 О, прости, я только что заметил, что это один из тех вопросов типа вопросов и ответов. Я новичок в этом сайте. Извиняюсь! Ваш ответ великолепен, я проголосовал за него - но также хотел предоставить более краткую версию для тех, кто хочет суть этого . Надеюсь, я не помешаю.
1
@ Mr.C64, в чем проблема с ОП, отвечающим на вопрос? Это просто еще один, правильный ответ.
mfontanini
1
@mfontanini: Нет абсолютно никаких проблем, если кто-то отправит ответ, даже лучше, чем мой. Конечная цель - сделать качественный вклад в сообщество (особенно для начинающих, которые могут чувствовать себя потерянными перед различными синтаксисами и различными опциями, которые предлагает C ++).
Mr.C64
4
Правильное средство всегда
for(auto&& elem : container)
Это будет гарантировать сохранение всей семантики.
Но что, если контейнер возвращает только изменяемые ссылки, и я хочу прояснить, что я не хочу изменять их в цикле? Разве я не должен тогда использовать, auto const &чтобы прояснить свои намерения?
RedX
@RedX: Что такое «изменяемая ссылка»?
Гонки легкости на орбите
2
@RedX: ссылки никогда не бывают const, и они никогда не изменяются. Во всяком случае, мой ответ вам да, я бы .
Гонки легкости на орбите
4
Хотя это может сработать, я считаю, что это плохой совет по сравнению с более тонким и продуманным подходом, данным превосходным и всеобъемлющим ответом Mr.C64, приведенным выше. Сокращение до наименьшего общего знаменателя - это не то, для чего нужен C ++.
Хотя первоначальной мотивацией цикла range-for могла быть простота итерации по элементам контейнера, синтаксис достаточно универсален, чтобы быть полезным даже для объектов, которые не являются чисто контейнерами.
Синтаксическое требование для цикла for состоит в том, чтобы range_expressionподдерживать begin()и end()как функции, так и функции-члены того типа, который он оценивает, или функции, не являющиеся членами, которые берут экземпляр типа.
В качестве надуманного примера можно сгенерировать диапазон чисел и выполнить итерацию по диапазону, используя следующий класс.
auto (const)(&) x = <expr>;
.auto
в целом, чем с диапазоном; Вы можете прекрасно использовать диапазон без каких-либоauto
!for (int i: v) {}
отлично в порядке. Конечно, большинство вопросов, которые вы поднимаете в своем ответе, может иметь больше общего с типом, чем сauto
... но из вопроса неясно, где находится болевая точка. Лично я бы боролся за снятиеauto
с вопроса; или, может быть, сделать явным, что независимо от того, используете ли выauto
тип или явно называете тип, вопрос сосредоточен на значении / ссылке.Ответы:
Давайте начнем с разграничения между наблюдением за элементами в контейнере и их изменением на месте.
Наблюдая за элементами
Давайте рассмотрим простой пример:
Приведенный выше код печатает элементы
int
вvector
:Теперь рассмотрим другой случай, когда векторные элементы представляют собой не просто целые числа, а экземпляры более сложного класса, с пользовательским конструктором копирования и т. Д.
Если мы используем приведенный выше
for (auto x : v) {...}
синтаксис с этим новым классом:вывод что-то вроде:
Как это можно прочитать из выходных данных, вызовы конструктора копирования выполняются во время итераций цикла на основе диапазона.
Это потому , что мы захватывая элементы из контейнера по значению (The
auto x
часть вfor (auto x : v)
).Это неэффективный код, например, если эти элементы являются экземплярами
std::string
, выделение памяти в куче может быть выполнено с дорогостоящими поездками в диспетчер памяти и т. Д. Это бесполезно, если мы просто хотим наблюдать за элементами в контейнере.Таким образом, лучше синтаксис доступен: захват с помощью
const
ссылки , то естьconst auto&
:Теперь вывод:
Без какого-либо ложного (и потенциально дорогого) вызова конструктора копирования.
Таким образом, при наблюдении за элементами в контейнере (т. Е. Для доступа только для чтения) следующий синтаксис подходит для простых типов, которые можно копировать дешево , например
int
,double
и т. Д .:Иначе, захват по
const
ссылке лучше в общем случае , чтобы избежать бесполезных (и потенциально дорогих) вызовов конструктора копирования:Изменение элементов в контейнере
Если мы хотим изменить элементы в контейнере с использованием диапазонов
for
, вышеприведенныйfor (auto elem : container)
иfor (const auto& elem : container)
синтаксис неверен.Фактически, в первом случае
elem
хранится копия исходного элемента, поэтому сделанные в нем модификации просто теряются и не сохраняются постоянно в контейнере, например:Вывод - это просто начальная последовательность:
Вместо этого попытка использования
for (const auto& x : v)
просто не в состоянии скомпилировать.g ++ выводит сообщение об ошибке примерно так:
Правильный подход в этом случае - захват без
const
ссылки:Выход (как и ожидалось):
Этот
for (auto& elem : container)
синтаксис работает также для более сложных типов, например, с учетомvector<string>
:выход:
Частный случай прокси-итераторов
Предположим, у нас есть
vector<bool>
, и мы хотим инвертировать логическое логическое состояние его элементов, используя приведенный выше синтаксис:Приведенный выше код не компилируется.
g ++ выводит сообщение об ошибке, похожее на это:
Проблема заключается в том, что
std::vector
шаблон специализируется наbool
, с реализацией , что пакеты сbool
˙s для оптимизации пространства (каждый булево значение хранится в один бит, восемь «Boolean» бит в байте).Из-за этого (поскольку невозможно вернуть ссылку на один бит),
vector<bool>
используется так называемый шаблон «прокси-итератор» . «Итератор прокси» - это итератор, который при разыменовании не дает обычногоbool &
, а вместо этого возвращает (по значению) временный объект , который является прокси-классом, преобразуемым вbool
. (См. Также этот вопрос и связанные с ним ответы здесь, на StackOverflow.)Чтобы изменить на месте элементы
vector<bool>
, необходимо использовать новый вид синтаксиса (использованиеauto&&
):Следующий код работает нормально:
и выводы:
Обратите внимание, что
for (auto&& elem : container)
синтаксис также работает в других случаях обычных (не-прокси) итераторов (например, дляvector<int>
или avector<string>
).(Как примечание, вышеупомянутый синтаксис "наблюдения"
for (const auto& elem : container)
отлично работает и для случая с итератором прокси.)Резюме
Приведенное выше обсуждение может быть кратко изложено в следующих рекомендациях:
Для наблюдения за элементами используйте следующий синтаксис:
Если объекты дешевы для копирования (например,
int
s,double
s и т. Д.), Можно использовать слегка упрощенную форму:Для изменения элементов на месте используйте:
Если контейнер использует «итераторы прокси» (например
std::vector<bool>
), используйте:Конечно, если необходимо создать локальную копию элемента внутри тела цикла, захват с помощью 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>
.)Итак, в общем коде могут быть предоставлены следующие рекомендации:
Для наблюдения за элементами используйте:
Для изменения элементов на месте используйте:
источник
auto&&
? Есть лиconst auto&&
?auto&&
, так как оноauto&
одинаково хорошо покрывает .Там нет правильного способа использовать
for (auto elem : container)
, илиfor (auto& elem : container)
илиfor (const auto& elem : container)
. Вы просто выражаете то, что хотите.Позвольте мне остановиться на этом подробнее. Давайте прогуляемся.
Это синтаксический сахар для:
Вы можете использовать это, если ваш контейнер содержит элементы, которые дешево копировать.
Это синтаксический сахар для:
Используйте это, например, для прямой записи в элементы контейнера.
Это синтаксический сахар для:
Как говорится в комментарии, просто для чтения. И это все, «правильно» при правильном использовании.
источник
Правильное средство всегда
Это будет гарантировать сохранение всей семантики.
источник
auto const &
чтобы прояснить свои намерения?const
, и они никогда не изменяются. Во всяком случае, мой ответ вам да, я бы .Хотя первоначальной мотивацией цикла range-for могла быть простота итерации по элементам контейнера, синтаксис достаточно универсален, чтобы быть полезным даже для объектов, которые не являются чисто контейнерами.
Синтаксическое требование для цикла for состоит в том, чтобы
range_expression
поддерживатьbegin()
иend()
как функции, так и функции-члены того типа, который он оценивает, или функции, не являющиеся членами, которые берут экземпляр типа.В качестве надуманного примера можно сгенерировать диапазон чисел и выполнить итерацию по диапазону, используя следующий класс.
Со следующей
main
функцией,можно было бы получить следующий вывод.
источник