Должен ли я использовать std :: for_each? - PullRequest
42 голосов
/ 07 июня 2011

Я всегда стараюсь больше узнать о языках, которые я использую (разные стили, рамки, шаблоны и т. Д.). Я заметил, что я никогда не использую std::for_each, поэтому я подумал, что, возможно, мне следует начать. Цель в таких случаях - расширить свой кругозор, а , а не , в некоторой степени улучшить код (удобочитаемость, выразительность, компактность и т. Д.).

Таким образом, с учетом этого контекста, будет хорошей идеей использовать std::for_each для простых задач, таких как, скажем, распечатка вектора:

for_each(v.begin(), v.end(), [](int n) { cout << n << endl; }

([](int n) - лямбда-функция). Вместо:

for(int i=0; i<v.size(); i++) { cout << v[i] << endl; }

Надеюсь, этот вопрос не кажется бессмысленным. Я предполагаю, что это почти задает больший вопрос ... должен ли промежуточный программист использовать языковую функцию, хотя ему на самом деле не нужно набирать на этот раз , но только для того, чтобы он мог лучше понять эту функцию на некоторое время это может на самом деле очень выиграть от этого. Хотя этот более крупный вопрос, вероятно, уже был задан (например, здесь ).

Ответы [ 9 ]

46 голосов
/ 07 июня 2011

Преимущество использования std::for_each вместо цикла for старой школы (или даже новомодного цикла C ++ 0x range- for): вы можете посмотреть на первое слово утверждения, и выточно знать, что делает оператор.

Когда вы видите for_each, вы знаете, что операция в лямбда-выражении выполняется ровно один раз для каждого элемента в диапазоне (при условии, что не генерируются исключения).Невозможно выйти из цикла раньше, чем будет обработан каждый элемент, и невозможно пропустить элементы или оценить тело цикла для одного элемента несколько раз.

С for loop, вы должны прочитать все тело цикла, чтобы знать, что он делает.В нем могут быть операторы continue, break или return, которые изменяют поток управления.Он может иметь операторы, которые изменяют итератор или индексную переменную (и).Невозможно узнать, не изучив весь цикл.

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

Обратите внимание, что на самом деле вы можете использовать std::copyАлгоритм здесь, если вы предпочитаете:

std::copy(v.begin(), v.end(), std::ostream_iterator<int>(std::cout, "\n"));
24 голосов
/ 07 июня 2011

Это зависит.

Сила for_each в том, что вы можете использовать его с любым контейнером, итераторы которого удовлетворяют концепции входного итератора, и, таким образом, он обычно используется в любом контейнере. Это повышает удобство обслуживания таким образом, что вы можете просто поменять контейнер и не нужно ничего менять. То же самое не относится к циклу над size вектора. Единственными другими контейнерами, с которыми вы могли бы поменять его без необходимости изменения цикла, был бы другой случайный доступ.

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

// substitute 'container' with a container of your choice
for(std::container<T>::iterator it = c.begin(); it != c.end(); ++it){
  // ....
}

Довольно долго, а? C ++ 0x избавляет нас от этой длины с ключевым словом auto:

for(auto it = c.begin(); it != c.end(); ++it){
  // ....
}

Уже приятнее, но все же не идеально. Вы звоните end на каждой итерации, и это можно сделать лучше:

for(auto it = c.begin(), ite = c.end(); it != ite; ++it){
  // ....
}

Хорошо выглядит сейчас. Тем не менее, дольше, чем эквивалент for_each версия:

std::for_each(c.begin(), c.end(), [&](T& item){
  // ...
});

С "эквивалентом", являющимся слегка субъективным, так как T в списке параметров лямбды может быть некоторым подробным типом, таким как my_type<int>::nested_type. Тем не менее, можно typedef обойти это. Честно говоря, я до сих пор не понимаю, почему лямбды не могли быть полиморфными с выводом типа ...


Теперь еще одна вещь, которую следует учитывать, это то, что for_each, само имя, уже выражает намерение. Он говорит, что ни один элемент в последовательности не будет пропущен, что может быть в случае с обычным циклом for.

Это подводит меня к другому моменту: поскольку for_each предназначен для выполнения всей последовательности и применения операции к каждому элементу в контейнере, он не предназначен для ранней обработки return s. или break с в целом. continue можно смоделировать с помощью оператора return от лямбды / функтора.

Итак, используйте for_each, где вы действительно хотите применить операцию к каждому элементу в коллекции.

С другой стороны, for_each может быть просто "устаревшим" с C ++ 0x благодаря удивительным циклам for, основанным на диапазоне (также называемых циклами foreach):

for(auto& item : container){
  // ...
}

Что намного короче (yay) и допускает все три варианта:

  • возвращение рано (даже с возвращаемым значением!)
  • разрыв из цикла и
  • пропуская некоторые элементы.
9 голосов
/ 07 июня 2011

Я бы вообще рекомендовал использовать std::for_each. Ваш пример для цикла не работает для контейнеров без произвольного доступа. Вы можете написать тот же цикл, используя итераторы, но обычно это боль из-за записи std::SomeContainerName<SomeReallyLongUserType>::const_iterator в качестве типа переменной итерации. std::for_each изолирует вас от этого, а также автоматически амортизирует вызов на end.

8 голосов
/ 07 июня 2011

ИМХО, вы должны попробовать эти новые функции в вашем тестовом коде .

В производственном коде вы должны попробовать функции, которые вам удобны. (т.е. если вы чувствуете себя комфортно с for_each, вы можете использовать его.)

3 голосов
/ 07 июня 2011

for_each - самый общий из алгоритмов, которые повторяются в последовательности, и, следовательно, наименее выразительный. Если цель итерации может быть выражена в виде transform, accumulate, copy, я чувствую, что лучше использовать конкретный алгоритм, а не общий for_each.

С новым диапазоном C ++ 0x для (поддерживается в gcc 4.6.0, попробуйте!), for_each может даже потерять свою нишу в качестве наиболее общего способа применения функции к последовательности.

1 голос
/ 05 июля 2017

Вы можете использовать for циклическую область видимости C ++ 11

Например:

 T arr[5];
 for (T & x : arr) //use reference if you want write data
 {
 //something stuff...
 }

Где T - любой тип, который вы хотите.

Это работает длявсе контейнеры в STL и классических массивах.

0 голосов
/ 05 июля 2017

Обратите внимание, что "традиционный" пример содержит ошибки:

for(int i=0; i<v.size(); i++) { cout << v[i] << endl; }

Это предполагает, что int всегда может представлять индекс каждого значения в векторе.На самом деле есть два пути, по которым это может пойти не так.

Во-первых, int может иметь более низкий ранг, чем std::vector<T>::size_type.На 32-битной машине int обычно имеют ширину 32 бита, но v.size() почти наверняка будет иметь ширину 64 бита.Если вам удастся вставить 2 ^ 32 элемента в вектор, ваш индекс никогда не достигнет конца.

Вторая проблема заключается в том, что вы сравниваете значение со знаком (int) со значением без знака (std::vector<T>::size_type).Таким образом, даже если они были одного и того же ранга, когда размер превышает максимальное целочисленное значение, индекс будет переполнен и вызовет неопределенное поведение.

Возможно, вы уже знали, что для этого вектора эти условия ошибки никогда не будут истинными.Но вам придется либо игнорировать, либо отключить предупреждения компилятора.И если вы отключите их, то вы не получите тех предупреждений, которые помогут вам найти реальные ошибки в другом месте вашего кода.(Я потратил много времени на отслеживание ошибок, которые должны были быть обнаружены этими предупреждениями компилятора, если код сделал возможным их включение.)

Итак, да, for_each (или любые другие соответствующие<algorithm>) лучше, потому что он избегает этого пагубного злоупотребления int с.Вы также можете использовать цикл for на основе диапазона или цикл на основе итератора с auto.

Дополнительным преимуществом использования <algorithm> s или итераторов, а не индексов является то, что это дает вам больше гибкости для изменения типов контейнеров.в будущем без рефакторинга всего кода, который его использует.

0 голосов
/ 07 июня 2011

Boost.Range упрощает использование стандартных алгоритмов. Для вашего примера вы можете написать:

boost::for_each(v, [](int n) { cout << n << endl; });

(или boost::copy с итератором ostream, как предлагается в других ответах).

0 голосов
/ 07 июня 2011

Хорошо ... это работает, но для печати вектора (или содержимого других типов контейнеров) я предпочитаю это:

std::copy(v.begin(), v.end(), std::ostream_iterator< int >( std::cout, " " ) );
...