Должен ли я использовать std:: for_each?
Я всегда стараюсь больше узнать о языках, которые я использую (разные стили, рамки, шаблоны и т.д.). Я заметил, что никогда не использовал 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; }
Надеюсь, этот вопрос не кажется бессмысленным. Я предполагаю, что он почти задает больший вопрос... если промежуточный программист использует языковую функцию, даже если ему действительно не нужно на на этот раз, а просто чтобы он мог лучше понять эту функцию для время, которое может в значительной степени выиграть от этого. Хотя этот более крупный вопрос, вероятно, уже был задан (например, здесь).
Ответы
Ответ 1
Преимущество использования std::for_each
вместо старой школы for
цикла (или даже цикла с новичком в С++ 0x range - for
): вы можете посмотреть первое слово инструкции, и вы точно знать, что делает инструкция.
Когда вы видите for_each
, вы знаете, что операция в лямбда выполняется ровно один раз для каждого элемента в диапазоне (при условии, что исключений не выбрасывается). Невозможно вырваться из цикла раньше, чем каждый элемент был обработан, и невозможно пропустить элементы или вычислить тело цикла для одного элемента несколько раз.
В цикле for
вы должны прочитать весь цикл цикла, чтобы узнать, что он делает. Он может иметь в нем continue
, break
или return
заявления, которые изменяют поток управления. У него могут быть инструкции, которые изменяют итератор или переменную индекса. Невозможно узнать, не исследуя весь цикл.
Херб Саттер обсудил преимущества использования алгоритмов и лямбда-выражений в недавней презентации для Северо-Западной группы пользователей С++.
Обратите внимание, что вы можете использовать алгоритм std::copy
здесь, если хотите:
std::copy(v.begin(), v.end(), std::ostream_iterator<int>(std::cout, "\n"));
Ответ 2
Это зависит.
Сила for_each
заключается в том, что вы можете использовать ее с любым контейнером, итераторы которого удовлетворяют концепции ввода итератора и, как таковой, могут использоваться в любом контейнере. Это увеличивает ремонтопригодность таким образом, что вы можете просто заменить контейнер и не нужно ничего менять. То же самое не выполняется для цикла над вектором size
. Единственными другими контейнерами, которые вы могли бы поменять, без необходимости менять цикл, будет другой случайный доступ.
Теперь, если вы сами выберете версию итератора, типичная версия будет выглядеть так:
// substitute 'container' with a container of your choice
for(std::container<T>::iterator it = c.begin(); it != c.end(); ++it){
// ....
}
Скорее длинный, а? С++ 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
. Хотя, можно обойти это. Честно говоря, я до сих пор не понимаю, почему лямбдам не разрешалось быть полиморфными с типом дедукции...
Теперь еще одна вещь, которую следует учитывать, состоит в том, что for_each
, само имя, уже выражает намерение. В нем говорится, что никакие элементы в последовательности не будут пропущены, что может иметь место с вашим обычным циклом.
Это приводит меня к другой точке: поскольку for_each
предназначен для выполнения всей последовательности и применения операции для каждого элемента в контейнере, он не предназначен для обработки ранних return
или break
в целом. continue
можно моделировать с помощью инструкции return
от лямбда/функтора.
Итак, используйте for_each
, где вы действительно хотите применить операцию для каждого элемента в коллекции.
На стороне примечание for_each
может быть просто "устаревшим" с С++ 0x благодаря удивительным диапазонам for-loops (также называемым петлями foreach):
for(auto& item : container){
// ...
}
Какой путь короче (yay) и разрешает все три варианта:
- возврат рано (даже с возвращаемым значением!)
- выход из цикла и
- пропуская некоторые элементы.
Ответ 3
Обычно я рекомендую использовать std::for_each
. Ваш пример цикла не работает для контейнеров без случайного доступа. Вы можете написать тот же цикл, используя итераторы, но обычно это боль из-за записи std::SomeContainerName<SomeReallyLongUserType>::const_iterator
в качестве типа переменной итерации. std::for_each
изолирует вас от этого, а также автоматически отменяет вызов end
.
Ответ 4
IMHO, вы должны попробовать эти новые функции в тестовом коде.
В производственном коде вы должны попробовать функции, с которыми вам комфортно. (т.е. если вы чувствуете себя комфортно с for_each
, вы можете использовать его.)
Ответ 5
for_each
является наиболее общим из алгоритмов, которые перебирают последовательность и, следовательно, наименее выразительны. Если цель итерации может быть выражена через transform
, accumulate
, copy
, я считаю, что лучше использовать конкретный алгоритм, а не общий for_each
.
С новым диапазоном С++ 0x (поддерживается в gcc 4.6.0, попробуйте это!), for_each
может даже потерять свою нишу в качестве наиболее общего способа применения функции к последовательности.
Ответ 6
Вы можете использовать for
обзор цикла С++ 11
Например:
T arr[5];
for (T & x : arr) //use reference if you want write data
{
//something stuff...
}
Где T - любой тип, который вы хотите.
Он работает для каждого контейнера в STL и классических массивах.
Ответ 7
Ну... это работает, но для печати вектора (или содержимого других типов контейнеров) я предпочитаю это:
std::copy(v.begin(), v.end(), std::ostream_iterator< int >( std::cout, " " ) );
Ответ 8
Boost.Range упрощает использование стандартных алгоритмов. Для вашего примера вы можете написать:
boost::for_each(v, [](int n) { cout << n << endl; });
(или boost::copy
с итератором ostream, как предложено в других ответах).
Ответ 9
Обратите внимание, что пример "традиционный" не работает:
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
). Поэтому, даже если они имеют одинаковый ранг, когда размер превышает максимальное целочисленное значение, тогда индекс будет переполняться и запускать поведение undefined.
Возможно, у вас есть предварительное знание о том, что для этого вектора эти условия ошибки никогда не будут истинными. Но вам придется либо игнорировать, либо отключать предупреждения компилятора. И если вы отключите их, тогда вы не получите преимущества тех предупреждений, которые помогут вам найти настоящие ошибки в другом месте вашего кода. (Я потратил много времени на отслеживание ошибок, которые должны были быть обнаружены этими предупреждениями компилятора, если бы код сделал возможным их включение.)
Итак, да, for_each
(или любой подходящий <algorithm>
) лучше, потому что он избегает этого пагубного злоупотребления int
s. Вы также можете использовать цикл, основанный на диапазоне, или петлю на основе итератора с авто.
Дополнительным преимуществом использования <algorithm>
или итераторов, а не индексов является то, что он дает вам больше гибкости для изменения типов контейнеров в будущем без реорганизации всего кода, который его использует.