C ++ STL: какой метод итерации по контейнеру STL лучше?

Некоторым из вас это может показаться несерьезным, но какой из следующих двух методов итерации по контейнеру STL лучше? Почему?

class Elem;
typedef vector<Elem> ElemVec;
ElemVec elemVec;

// Method 0
for (ElemVec::iterator i = elemVec.begin(); i != elemVec.end(); ++i)
{
    Elem& e = *i;
    // Do something
}

// Method 1
for (int i = 0; i < elemVec.size(); ++i)
{
    Elem& e = elemVec.at(i);
    // Do something
}

Метод 0 кажется более чистым STL, но метод 1 достигает того же с меньшим количеством кода. Простая итерация по контейнеру - это то, что появляется везде в любом месте исходного кода. Итак, я склонен выбрать метод 1, который, кажется, уменьшает визуальный беспорядок и размер кода.

PS: Я знаю, что итераторы могут делать гораздо больше, чем простой индекс. Но, пожалуйста, держите ответ / обсуждение сосредоточенным на простой итерации над контейнером, как показано выше.


person Ashwin Nanjappa    schedule 04.04.2009    source источник


Ответы (9)


Первая версия работает с любым контейнером и поэтому более полезна в функциях шаблона, которые принимают любой контейнер в качестве параметра. Кроме того, возможно, он немного более эффективен даже для векторов.

Вторая версия работает только для векторов и других контейнеров с целочисленным индексом. Это было бы несколько более идиоматично для этих контейнеров, будет легко понятно новичкам в C ++ и будет полезно, если вам нужно сделать что-то еще с индексом, что не редкость.

Боюсь, что, как обычно, простого ответа «этот лучше» не существует.

person Community    schedule 04.04.2009
comment
Спасибо, Нил. Мой код обычно работает не с шаблонами, а с векторами, тип которых известен. Не могли бы вы пояснить, почему метод 0 более эффективен в своем ответе? - person Ashwin Nanjappa; 04.04.2009
comment
Это может быть немного более эффективным, если итератор фактически реализован непосредственно как указатель. Обратите внимание на использование слов может и немного - вам нужно измерить, чтобы быть уверенным. - person ; 04.04.2009
comment
Да, действительно итератор - не более чем указатель для большинства контейнеров. Но как это ускорить код? AFAIK даже метод 1 оказывается арифметическим указателем, не так ли? - person Ashwin Nanjappa; 04.04.2009
comment
@ash да, но есть арифметические действия (ptr + index), а также (index ++), не говоря уже о том, что [] может быть вызовом функции, если он не был встроен. Но как я уже сказал - мерять нужно. - person ; 04.04.2009
comment
Для справки, я никогда не видел ощутимой разницы в производительности. - person Robert Gould; 04.04.2009
comment
Метод indexed (at) 0 будет значительно медленнее для списка, верно? Если он реализован как связанный список, это O (N). - person Andrew Jaffe; 04.04.2009
comment
@andrew правильно - я все время забываю, что список поддерживает индексированный доступ, так как я не могу представить, чтобы кто-то был достаточно безумным, чтобы его использовать :-) - person ; 04.04.2009
comment
На самом деле @neil, я не думаю, что list действительно реализует at () или operator [], после проверки документации в Интернете. Но даже size () - O (N). - person Andrew Jaffe; 04.04.2009
comment
@andrew Вы правы - показывает, как часто я использую список, т.е. никогда! - person ; 04.04.2009
comment
Да, моя нет разницы в производительности применима только к вектору по сравнению с вектором. В любом случае я использую карты для производительности, не обращая внимания на код, в этом случае единственным вариантом является метод 0 - person Robert Gould; 04.04.2009

Если вы не возражаете против (очень?) Небольшой потери эффективности, я бы рекомендовал использовать Boost.Foreach

BOOST_FOREACH( Elem& e, elemVec )
{
    // Your code
}
person Benoît    schedule 04.04.2009
comment
Я сейчас в основном неграмотный Boost. Спасибо за этот совет, Бенуа! Это хранитель :-) - person Ashwin Nanjappa; 04.04.2009
comment
+1, Бенуа, у меня повсюду петли, и BOOST_FOREACH поддерживает меня в здравом уме после использования других языков с настоящей поддержкой foreach - person philsquared; 04.04.2009
comment
В C ++ также есть настоящая поддержка foreach: std :: for_each. Синтаксис немного отличается;) - person jalf; 04.04.2009
comment
Джалф: В STL есть for_each, но это вряд ли способ использования большинства циклов, поскольку он требует определения другой функции. - person Ashwin Nanjappa; 04.04.2009
comment
Когда лямбда идет с C ++ 09, stl :: foreach будет весьма полезен - person Robert Gould; 04.04.2009
comment
да, C ++ 1x в любом случае будет содержать собственный цикл for-each for (Elem & e: elemVec) ...; работа даже с массивами без этих начальных / конечных беспорядков - person Johannes Schaub - litb; 04.04.2009

Метод 0 быстрее и поэтому рекомендуется.

Метод 1 использует size (), которому разрешено быть O (1), в зависимости от контейнера и реализации stl.

person Stefan    schedule 04.04.2009
comment
Спасибо, Стефан, я забыл про размер () :-) - person Ashwin Nanjappa; 04.04.2009
comment
size () должен быть O (1) в стандартном совместимом векторе. И он не менее эффективен, чем v.end (), так как, вероятно, будет встроен. Эффективность здесь такая же (разница не более пары инструкций на доступ) - person David Rodríguez - dribeas; 04.04.2009
comment
@ DavidRodríguez-dribeas: Эффективность обычно не такая же, потому что первый метод требует дополнительной загрузки указателя (т.е. загрузки указателя на данные перед добавлением смещения). Если у вас есть какой-либо другой код рядом с этим, компилятор может запутаться в псевдониме и избежать кеширования этого указателя, заставляя вас выдавать в два раза больше загрузок из памяти, чем вам нужно. Это вряд ли произойдет с тривиальным циклом, но на практике они не встречаются. - person user541686; 26.08.2013
comment
@Mehrdad: Кеширование size(), вероятно, не является проблемой с исходным кодом (комментарий был адресован только кешированию size()). В OP доступ к вектору осуществляется через at(), что включает в себя дополнительную ветвь в коде и некоторый дополнительный код. - person David Rodríguez - dribeas; 26.08.2013
comment
@ DavidRodríguez-dribeas: Я сказал кеширование указателя. size() не является указателем. Я говорил о begin() и end() - использование итераторов / указателей обычно быстрее, чем индексация, потому что для этого требуется меньше нагрузок. (at() явно медленнее, но я говорил о регулярном неконтролируемом индексировании.) - person user541686; 26.08.2013

Лучше всего использовать следующий метод итерации по контейнеру стандартной библиотеки.

Используйте c ++ 11 (и не только) for-loop на основе диапазона с auto спецификатор:

// Method 2
for (auto& e: elemVec)
{
    // Do something with e...
}

Это похоже на ваш Method 0, но требует меньше ввода, меньше обслуживания и работает с любым контейнером, совместимым с std::begin() и std::end(), включая plain- старые массивы.

person Johnsyweb    schedule 29.04.2012
comment
-1, auto & является правильным эквивалентом для этого Q, также этот код просто неправильный, я не итератор - person NoSenseEtAl; 14.02.2014
comment
@NoSenseEtAl: Спасибо, что помогли мне улучшить мой ответ ☺ - person Johnsyweb; 16.02.2014

Еще несколько преимуществ метода 0:

  • если вы перейдете из вектора в другой контейнер, цикл останется прежним,
  • легко перейти от итератора к const_iterator, если вам нужно,
  • когда появится c ++ 0x, автоматическая типизация уменьшит беспорядок в коде.

Основным недостатком является то, что во многих случаях вы сканируете два контейнера, и в этом случае индекс чище, чем сохранение двух итераторов.

person David Lehavi    schedule 04.04.2009
comment
Спасибо, Дэвид. Думаю, вы имели в виду метод 0 ;-) - person Ashwin Nanjappa; 04.04.2009
comment
Да, Дэвид, не могли бы вы отредактировать свой ответ, чтобы отразить это? Спасибо :-) - person Ashwin Nanjappa; 04.04.2009

Метод 0 по нескольким причинам.

  • Он лучше выражает ваше намерение, что способствует оптимизации компилятора, а также удобочитаемости.
  • Поочередные ошибки менее вероятны
  • Это работает, даже если вы замените вектор списком, в котором нет оператора []

Конечно, лучшим решением часто будет решение 2: один из стандартных алгоритмов. std :: for_each, std :: transform, std :: copy или все, что вам нужно. У этого есть еще несколько преимуществ:

  • Он еще лучше выражает ваше намерение и допускает некоторые значительные оптимизации компилятора (безопасный SCL MS выполняет проверку границ для ваших методов 0 и 1, но пропускает его в алгоритмах std)
  • Это меньше кода (по крайней мере, на сайте вызова. Конечно, вам нужно написать функтор или что-то еще, чтобы заменить тело цикла, но на сайте использования код немного очищается, что, вероятно, является наиболее важным. .

В общем, избегайте чрезмерного определения кода. Укажите именно то, что вы хотите сделать, и ничего больше. Обычно лучше всего подходят алгоритмы std, но даже без них, если вам не нужен индекс i, зачем он? В этом случае используйте итераторы.

person jalf    schedule 04.04.2009

Так совпало, что недавно я провел тест скорости (заполнив 10 * 1024 * 1024 целых с помощью rand ()).
Это 3 запуска, время в наносекундах

vect[i] time      : 373611869  
vec.at(i) time    : 473297793  
*it = time        : 446818590  
arr[i] time       : 390357294  
*ptr time         : 356895778  

ОБНОВЛЕНИЕ: добавлен stl-алгоритм std :: generate, который, кажется, работает быстрее всего из-за специальной оптимизации итератора (VC ++ 2008). время в микросекундах.

vect[i] time      : 393951
vec.at(i) time    : 551387
*it = time        : 596080
generate = time   : 346591
arr[i] time       : 375432
*ptr time         : 334612

Вывод: используйте стандартные алгоритмы, они могут быть быстрее, чем явный цикл! (а также хорошая практика)

Обновление: указанное выше время было в ситуации, связанной с вводом-выводом, я провел те же тесты с привязкой к ЦП (итерация по относительно короткому вектору, который должен многократно помещаться в кеш, умножение каждого элемента на 2 и запись обратно в вектор )

//Visual Studio 2008 Express Edition
vect[i] time      : 1356811
vec.at(i) time    : 7760148
*it = time        : 4913112
for_each = time   : 455713
arr[i] time       : 446280
*ptr time         : 429595

//GCC
vect[i] time      : 431039
vec.at(i) time    : 2421283
*it = time        : 381400
for_each = time   : 380972
arr[i] time       : 363563
*ptr time         : 365971  

Интересно, что итераторы и operator [] значительно медленнее в VC ++ по сравнению с for_each (который, похоже, ухудшает итераторы до указателей с помощью некоторой магии шаблонов для повышения производительности).
В GCC время доступа только хуже для at (), что нормально , потому что это единственная функция тестов с проверкой диапазона.

person qwerty    schedule 04.04.2009
comment
Практически нет статистической разницы. Все, что составляет около 10%, не повлияет на фактическое использование программы, если только это не находится в часто используемом жестком цикле. Промах кеша, и время будет равно - person Robert Gould; 04.04.2009
comment
Если вы определите #define _SECURE_SCL 0 #define _SECURE_SCL_THROWS 0, не будет никакой разницы между производительностью указателя и итератора. - person Vadim Ferderer; 05.04.2009

Это зависит от того, какой тип тары. Для vector, вероятно, не имеет значения, что вы используете. Метод 0 стал более идиоматичным, но, как все говорят, это не большая разница.

Если бы вы решили использовать list, вместо этого метод 1, в принципе, был бы O(N), но на самом деле метода list at() нет, поэтому вы даже не можете сделать это таким образом. (Так что на каком-то уровне ваш вопрос складывается в колоду.)

Но это само по себе является еще одним аргументом для метода 0: он использует один и тот же синтаксис для разных контейнеров.

person Andrew Jaffe    schedule 04.04.2009

Возможность, не рассмотренная выше: в зависимости от деталей «Сделать что-нибудь», можно использовать метод 0 и метод 1 одновременно, вам не нужно выбирать:

for (auto i = elemVec.begin(), ii = 0; ii < elemVec.size(); ++i, ++ii)
{
    // Do something with either the iterator i or the index ii
}

Таким образом, поиск индекса или доступ к соответствующему члену выполняются с тривиальной сложностью.

person user2747939    schedule 14.02.2017