Является ли цикл for на основе диапазона устаревшим для многих простых алгоритмов?

81

Решение алгоритма:

std::generate(numbers.begin(), numbers.end(), rand);

Решение для цикла на основе диапазона:

for (int& x : numbers) x = rand();

Зачем мне использовать более подробные std::generateциклы for на основе диапазона в C ++ 11?

fredoverflow
источник
14
Сочетаемость? Да ладно, алгоритмы с итераторами в любом случае часто невозможно компоновать ... :(
Р. Мартиньо Фернандес
2
... чего нет begin()и end()?
the_mandrill
6
@jrok Я ожидаю, что к настоящему времени у многих людей уже есть rangeфункция в своем наборе инструментов. (ie for(auto& x : range(first, last)))
Р. Мартиньо Фернандес
14
boost::generate(numbers, rand); // ♪
Xeo
5
@JamesBrock Мы часто обсуждали это в чате C ++ (это должно быть где-то в стенограммах: P). Основная проблема заключается в том, что алгоритмы часто возвращают один итератор и принимают два итератора.
R. Martinho Fernandes

Ответы:

79

Первая версия

std::generate(numbers.begin(), numbers.end(), rand);

сообщает нам, что вы хотите создать последовательность значений.

Во второй версии читателю придется разобраться в этом самому.

Экономия на печатании обычно неоптимальна, так как чаще всего теряется время чтения. Большая часть кода читается гораздо больше, чем набирается.

Бо Перссон
источник
13
Экономия на наборе текста? Ох, я понял. Почему, почему у нас один и тот же термин для «проверки работоспособности во время компиляции» и «нажатия клавиш на клавиатуре»? :)
fredoverflow
25
« Экономия на печатании обычно неоптимальна » Вздор; все дело в том, какую библиотеку вы используете. std :: generate является длинным, потому что вам нужно указать numbersдважды без причины. Следовательно: boost::range::generate(numbers, rand);. Нет причин, по которым у вас не может быть и более короткого, и более разборчивого кода в хорошо построенной библиотеке.
Никол Болас
9
Все в глазах читателя. Ибо версия цикла понятна с большинством программных фонов: поместите значение rand в каждый элемент коллекции. Std :: generate требует знания последних версий C ++ или предположения, что на самом деле генерировать означает «изменять элементы», а не «возвращать сгенерированные значения».
hyde
2
Если вы хотите изменить только часть контейнера, вы можете std::generate(number.begin(), numbers.begin()+3, rand), не так ли? Так что, я думаю, numberиногда бывает полезно указать дважды.
Марсон Мао
7
@MarsonMao: если у вас есть только два аргумента std::generate(), вы можете вместо этого std::generate(slice(number.begin(), 3), rand)или даже лучше использовать гипотетический синтаксис нарезки диапазона, например, std::generate(number[0:3], rand)который удаляет повторение, но при numberэтом позволяет гибкую спецификацию части диапазона. Сделать обратное, начиная с трех аргументов std::generate(), более утомительно.
Ли Райан
42

Независимо от того, основан ли цикл for на диапазоне или нет, это вообще не имеет значения, он только упрощает код внутри круглых скобок. Алгоритмы более ясны, поскольку они показывают намерение .

Давид Родригес - дрибеас
источник
30

Лично мое первоначальное прочтение:

std::generate(numbers.begin(), numbers.end(), rand);

это «мы назначаем все в диапазоне. Диапазон равен numbers. Присвоенные значения случайны».

Мое первое прочтение:

for (int& x : numbers) x = rand();

это «мы делаем что-то для всего в диапазоне. Диапазон есть numbers. Что мы делаем, так это присваиваем случайное значение».

Они чертовски похожи, но не идентичны. Одна вероятная причина, по которой я мог бы спровоцировать первое чтение, состоит в том, что я думаю, что наиболее важным фактом в этом коде является то, что он присваивает диапазон. Итак, вот ваше «зачем мне…». Я использую, generateпотому что в C ++ std::generateозначает «присвоение диапазона». Как, кстати,std::copy , разница между ними заключается в том, что вы назначаете.

Однако есть смешивающие факторы. Циклы for, основанные на диапазоне, по своей сути имеют более прямой способ выражения диапазона numbers, чем алгоритмы на основе итераторов. Вот почему люди работают с библиотеками алгоритмов на основе диапазонов: boost::range::generate(numbers, rand);выглядит лучше, чемstd::generate версия.

В отличие от этого, int&в вашем цикле for на основе диапазона есть морщинка. Что, если тип значения диапазона не является типом значения int, тогда мы делаем здесь что-то раздражающе тонкое, что зависит от его преобразования int&, в то время как generateкод зависит только от того, что возврат может randбыть назначен элементу. Даже если тип значения есть int, я все равно могу остановиться, чтобы подумать, так оно или нет. Следовательно auto, это откладывает размышления о типах до тех пор, пока я не увижу, что назначается - auto &xя говорю: «возьмите ссылку на элемент диапазона, какой бы тип он ни имел». Еще в C ++ 03, алгоритмы (потому что они шаблоны функций) были способом скрыть точные типы, теперь они путь.

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

Стив Джессоп
источник
Вы когда-нибудь видели определяемый пользователем тип с символом operator int&()? :)
fredoverflow
@FredOverflow замените int&на, SomeClass&и теперь вам нужно беспокоиться о том, что операторы преобразования и конструкторы с одним параметром не отмечены explicit.
TemplateRex
@FredOverflow: не думаю. Вот почему, если это когда-нибудь произойдет, я не буду этого ожидать, и как бы я ни был параноиком сейчас, он укусит меня, если я не подумаю об этом тогда ;-) Прокси-объект мог бы работать с перегрузкой operator int&()и operator int const &() const, но опять же может работать с перегрузкой operator int() constи operator=(int).
Стив Джессоп,
1
@rhalbersma: Я не думаю, что вам нужно беспокоиться о конструкторах, поскольку неконстантная ссылка не привязывается к временной. Это только операторы преобразования в ссылочные типы.
Стив Джессоп
23

На мой взгляд, Эффективный пункт 43 STL: «Предпочитайте вызовы алгоритмов рукописным циклам». по-прежнему хороший совет.

Обычно я пишу функции-обертки, чтобы избавиться от begin()/ end()hell. Если вы это сделаете, ваш пример будет выглядеть так:

my_util::generate(numbers, rand);

Я считаю, что он превосходит цикл for на основе диапазона как в передаче намерения, так и в удобочитаемости.


Сказав это, я должен признать, что в C ++ 98 некоторые вызовы алгоритмов STL приводили к непроизносимому коду, и последующее «Предпочитать вызовы алгоритмов рукописным циклам» не казалось хорошей идеей. К счастью, лямбды это изменили.

Рассмотрим следующий пример из Herb Sutter: Lambdas, Lambdas Everywhere .

Задача: найти первый элемент в v, то есть > xи < y.

Без лямбд:

auto i = find_if( v.begin(), v.end(),
bind( logical_and<bool>(),
bind(greater<int>(), _1, x),
bind(less<int>(), _1, y) ) );

С лямбдой

auto i=find_if( v.begin(), v.end(), [=](int i) { return i > x && i < y; } );
Али
источник
1
Немного ортогонально к вопросу. Только первое предложение решает вопрос.
Дэвид Родригес - dribeas
@ DavidRodríguez-dribeas Да. Вторая половина объясняет, почему я считаю, что пункт 43 по-прежнему является хорошим советом.
Али
С Boost.Lambda это даже лучше, чем с лямбда-функциями C ++: auto i = find_if (v.begin (), v.end (), _1> x && _1 <y);
sdkljhdf hda
1
+1 за фантики. Делаем то же самое. Должен был быть в стандарте с первого дня (а может, со второго ...)
Macke
22

На мой взгляд, ручной цикл, хотя и может уменьшить многословие, не читается:

for (int& x : numbers) x = rand();

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

Намерение становится намного яснее, когда вы используете std::generate.

1. Инициализировать в данном контексте означает придать значимое значение элементам контейнера.

Наваз
источник
5
Разве это не потому, что вы не привыкли к циклам for на основе диапазона? Мне кажется довольно ясным, что это утверждение присваивается каждому элементу в диапазоне. Понятно, что generate делает то же самое, если вы знакомы std::generate, что можно предположить о программисте на C ++ (если они не знакомы, они найдут его, результат тот же).
Стив Джессоп
4
@SteveJessop: Этот ответ не отличается от двух других. Это требует от читателя немного больше усилий и немного больше подвержено ошибкам (что, если вы забудете один &символ?). Преимущество алгоритмов в том, что они показывают намерение, а с циклами вы должны это сделать. Если в реализации цикла есть ошибка, неясно, является ли это ошибкой или она была преднамеренной.
Дэвид Родригес - dribeas
1
@ DavidRodríguez-dribeas: этот ответ существенно отличается от двух других, ИМО. Он пытается вникнуть в причину , по которой автор находит один фрагмент кода более ясным / понятным, чем другой. Остальные утверждают это без анализа. Вот почему я нахожу это достаточно интересным, чтобы ответить на него :-)
Стив Джессоп
1
@SteveJessop: Вы должны заглянуть в тело цикла, чтобы прийти к выводу, что вы фактически генерируете числа, но в случае std::generate, просто взглянув, можно сказать, что что- то генерируется этой функцией; на что то что-то отвечает третий аргумент функции. Я думаю, это намного лучше.
Nawaz
1
@SteveJessop: Значит, вы принадлежите к меньшинству. Я бы написал код, который большинству понятнее: P. И последнее: я нигде не говорил, что другие будут читать цикл так же, как я. Я сказал (скорее имел в виду ), что это один из способов чтения цикла, который вводит меня в заблуждение, и поскольку там есть тело цикла, разные программисты будут читать его по-разному, чтобы понять, что там происходит; они могут возражать против использования такой петли по разным причинам, каждая из которых может быть правильной в соответствии с их восприятием.
Nawaz
9

Есть некоторые вещи, которые нельзя делать (просто) с циклами на основе диапазона, которые могут использовать алгоритмы, принимающие итераторы в качестве входных данных. Например сstd::generate :

Заполните контейнер до limit(исключено, limitявляется действующим итератором numbers) переменными из одного распределения, а остальные - переменными из другого распределения.

std::generate(numbers.begin(), limit, rand1);
std::generate(limit, numbers.end(), rand2);

Алгоритмы на основе итераторов позволяют лучше контролировать диапазон, в котором вы работаете.

Khaur
источник
8
Хотя причина удобочитаемости - ОГРОМНАЯ причина предпочтения алгоритмов, это единственный ответ, который показывает, что цикл for-based на основе диапазона является лишь подмножеством того, что есть алгоритмы , и, следовательно, не может ничего не рекомендовать ...
K-балл
6

В конкретном случае std::generateя согласен с предыдущими ответами по проблеме читабельности / намерения. std :: generate мне кажется более понятной версией. Но признаю, что это в некотором роде дело вкуса.

Тем не менее, у меня есть еще одна причина не отказываться от std :: algorithm - есть определенные алгоритмы, которые специализируются на некоторых типах данных.

Простейшим примером будет std::fill. Общая версия реализована как цикл for в указанном диапазоне, и эта версия будет использоваться при создании экземпляра шаблона. Но не всегда. Например, если вы предоставите ему диапазон, который является std::vector<int>- часто он действительно будет вызывать memsetпод капотом, давая гораздо более быстрый и лучший код.

Итак, я пытаюсь разыграть здесь карту эффективности.

Ваш рукописный цикл может быть таким же быстрым, как версия std :: algorithm, но вряд ли быстрее. Более того, std :: algorithm может быть специализирован для определенных контейнеров и типов, и это делается под чистым интерфейсом STL.

Сергей Носов
источник
3

Я бы ответил, может быть, и нет. Если мы говорим о C ++ 11, то возможно (скорее нет). Например std::for_each, очень неприятно использовать даже с лямбдами:

std::for_each(c.begin(), c.end(), [&](ExactTypeOfContainedValue& x)
{
    // do stuff with x
});

Но намного лучше использовать диапазон на основе:

for (auto& x : c)
{
    // do stuff with x
}

С другой стороны, если мы говорим о C ++ 1y, то я бы сказал, что нет, алгоритмы не будут устаревать на основе диапазона для. В стандартном комитете C ++ есть исследовательская группа, которая работает над предложением о добавлении диапазонов в C ++, а также ведется работа над полиморфными лямбдами. Диапазоны избавят от необходимости использовать пару итераторов, а полиморфная лямбда позволит вам не указывать точный тип аргумента лямбда. Это означает, что это std::for_eachможно использовать так (не воспринимайте это как твердый факт, это просто то, как сегодня выглядят сны):

std::for_each(c.range(), [](x)
{
    // do stuff with x
});
sdkljhdf hda
источник
Итак, в последнем случае преимущество алгоритма будет заключаться в том, что при записи []с лямбда- выражением вы указываете нулевой захват? То есть, по сравнению с простым написанием тела цикла, вы изолировали кусок кода от контекста поиска переменной, в котором он лексически появляется. Изоляция обычно полезна для читателя, о чем меньше думать во время чтения.
Стив Джессоп
1
Дело не в захвате. Дело в том, что с полиморфной лямбдой вам не нужно явно указывать, что такое тип x.
sdkljhdf hda
1
В этом случае мне кажется, что в этом гипотетическом C ++ 1y for_eachвсе еще бессмысленно даже при использовании с лямбдой. foreach + захват лямбда-выражения в настоящее время является подробным способом написания цикла for на основе диапазона, и он становится немного менее подробным, но все же более подробным, чем цикл. Не то чтобы я думаю, что вам следует защищаться for_each, конечно, но еще до того, как увидел ваш ответ, я подумал, что если бы спрашивающий хотел обойти алгоритмы, он мог бы выбрать for_eachсамую мягкую из всех возможных целей ;-)
Стив Джессоп
Не собираюсь защищать for_each, но у него есть одно крошечное преимущество перед основанным на диапазоне for - вы можете сделать его параллельным более легко, просто добавив к нему префикс parallel_, чтобы превратить его в parallel_for_each(если вы используете PPL и предполагаете, что это потокобезопасно) . :-D
sdkljhdf hda
@lego Ваше «маленькое» преимущество действительно является «большим» преимуществом, если обобщить его до того факта, что реализация std::algorithms скрыта за их интерфейсом и может быть произвольно сложной (или произвольно оптимизированной).
Christian Rau
1

Следует отметить, что алгоритм выражает то, что сделано, а не как.

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

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

Седрик
источник
1

Цикл for на основе диапазона - это именно то, что вам нужно. Пока конечно стандарт не изменится.

Алгоритм - это функция. Функция, которая предъявляет некоторые требования к своим параметрам. Требования сформулированы в стандарте, что позволяет, например, реализовать реализацию, которая использует все доступные потоки выполнения и автоматически ускоряет работу.

Томек
источник