Стоит ли отдавать предпочтение алгоритмам STL, а не ручным циклам? - PullRequest
25 голосов
/ 25 сентября 2008

Кажется, я вижу здесь больше циклов for для итераторов в вопросах и ответах, чем for_each (), transform () и тому подобное. Скотт Мейерс предполагает, что алгоритмы stl предпочтительнее , или, по крайней мере, он сделал это в 2001 году. Конечно, их использование часто означает перемещение тела цикла в функцию или функциональный объект. Кто-то может посчитать это неприемлемым осложнением, а кто-то может решить, что лучше решить проблему.

Итак ... должны ли алгоритмы STL быть более предпочтительными, чем циклы, свернутые вручную?

Ответы [ 11 ]

19 голосов
/ 25 сентября 2008

Это зависит от:

  • Требуется ли высокая производительность
  • Читаемость цикла
  • Сложен ли алгоритм

Если цикл не является узким местом, а алгоритм прост (например, for_each), то для текущего стандарта C ++ я бы предпочел циклический цикл для удобства чтения. (Местность логики является ключевой.)

Однако теперь, когда C ++ 0x / C ++ 11 поддерживается некоторыми крупными компиляторами, я бы сказал, использовать алгоритмы STL, потому что теперь они допускают лямбда-выражения - и, следовательно, локальность логики.

9 голосов
/ 30 января 2010

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

Давайте рассмотрим пример. Здесь у нас есть группа детей, и мы хотим установить для их «Foo Count» какое-то значение. Стандартный цикл for, итераторный подход:

for (vector<Child>::iterator iter = children.begin();
    iter != children.end();
    ++iter)
{
    iter->setFooCount(n);
}

Что, да, это довольно ясно, и определенно не плохой код. Вы можете понять это, просто немного посмотрев на это. Но посмотрите, что мы можем сделать с соответствующим функтором:

for_each(children.begin(), children.end(), SetFooCount(n));

Ух ты, это говорит именно то, что нам нужно. Вам не нужно это выяснять; Вы сразу же знаете, что он устанавливает «количество Foo» для каждого ребенка. (Было бы еще яснее, если бы нам не понадобилась ерунда .begin () / .end (), но у вас не может быть всего, и они не советовались со мной при создании STL.)

Конечно, вам нужно определить этот магический функтор, SetFooCount, но его определение довольно стандартно:

class SetFooCount
{
public:
    SetFooCount(int n) : fooCount(n) {}

    void operator () (Child& child)
    {
        child.setFooCount(fooCount);
    }

private:
    int fooCount;
};

В целом это больше кода, и вам нужно посмотреть в другом месте, чтобы точно узнать, что делает SetFooCount. Но поскольку мы назвали его правильно, в 99% случаев нам не нужно искать код для SetFooCount. Мы предполагаем, что он делает то, что говорит, и нам нужно только взглянуть на строку for_each.

Что мне действительно нравится, так это то, что использование алгоритмов приводит к смене парадигмы. Вместо того, чтобы думать о списке как о коллекции объектов, и делать что-то с каждым элементом списка, вы думаете о списке как о первоклассной сущности и работаете непосредственно с самим списком. Цикл for выполняет итерацию по списку, вызывая функцию-член для каждого элемента для установки счетчика Foo. Вместо этого я делаю одну команду, которая устанавливает Foo Count каждого элемента в списке. Это тонко, но когда вы смотрите на лес, а не на деревья, вы получаете больше силы.

Поэтому, немного подумав и осторожно назвав, мы можем использовать алгоритмы STL для создания более чистого и ясного кода и начать думать на менее детальном уровне.

8 голосов
/ 26 сентября 2008

std::foreach - это тот код, который заставил меня проклясть STL много лет назад.

Я не могу сказать, лучше ли это, но мне больше нравится иметь код моего цикла в преамбуле цикла. Для меня это сильное требование . И конструкция std::foreach не позволит мне этого (как ни странно, версии Java или C # для foreach - это круто, насколько я понимаю) очень очень важно).

Так что я буду использовать foreach только в том случае, если с ним уже есть только читаемый / понятный алгоритм. Если нет, нет, я не буду. Но это вопрос вкуса, я полагаю, поскольку мне, возможно, следует постараться понять и научиться разбирать все это ...

Обратите внимание, что люди в толпе, по-видимому, чувствовали себя примерно так же, поскольку они написали BOOST_FOREACH:

#include <string>
#include <iostream>
#include <boost/foreach.hpp>

int main()
{
    std::string hello( "Hello, world!" );

    BOOST_FOREACH( char ch, hello )
    {
        std::cout << ch;
    }

    return 0;
}

См .: http://www.boost.org/doc/libs/1_35_0/doc/html/foreach.html

6 голосов
/ 25 сентября 2008

Это действительно одна вещь, которую Скотт Мейерс ошибся.

Если есть фактический алгоритм, который соответствует тому, что вам нужно сделать, тогда, конечно, используйте алгоритм.

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

Есть несколько других опций, таких как boost :: bind или boost :: lambda, но это действительно сложные вещи для метапрограммирования шаблонов, они не очень хорошо работают с отладкой и пошаговым просмотром кода, поэтому их вообще следует избегать. *

Как уже упоминали другие, все это изменится, когда лямбда-выражения станут первоклассным гражданином.

4 голосов
/ 26 сентября 2008

Цикл for является обязательным, алгоритмы декларативными. Когда вы пишете std::max_element, становится очевидным, что вам нужно, когда вы используете цикл для достижения того же, это не обязательно так.

Алгоритмы также могут иметь небольшое преимущество в производительности. Например, при обходе std::deque специализированный алгоритм может избежать избыточной проверки того, перемещает ли данный инкремент указатель за границу фрагмента.

Однако сложные выражения функторов быстро делают вызовы алгоритма нечитаемыми. Если явный цикл более читабелен, используйте его. Если вызов алгоритма может быть выражен без десятиуровневых выражений связывания, то все равно предпочитайте его. Читаемость важнее, чем производительность здесь , потому что этот вид оптимизации - то, что Кнут так классно приписывает Hoare; вы сможете без проблем использовать другую конструкцию, если поймете, что это узкое место.

3 голосов
/ 26 сентября 2008

Я большой поклонник алгоритмов STL в принципе , но на практике это слишком громоздко. К тому времени, когда вы определите свои классы функторов / предикатов, две строки цикла могут превратиться в 40+ строк кода, которые вдруг в 10 раз сложнее понять.

К счастью, в C ++ 0x с лямбда-функциями, auto и новым синтаксисом for дела пойдут на тонну . Оформить заказ C ++ 0x Обзор в Википедии.

3 голосов
/ 25 сентября 2008

Зависит от того, что если алгоритм не принимает функтор, всегда используйте версию алгоритма std. Вам проще и понятнее писать.

Для алгоритмов, которые принимают функторы, обычно нет, пока не могут использоваться лямбды C ++ 0x. Если функтор маленький, а алгоритм сложный (большинство - нет), то, возможно, все же лучше использовать алгоритм std.

2 голосов
/ 25 сентября 2008

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

Насколько эффективнее стандарт для цикла над вектором:

int weighted_sum = 0;
for (int i = 0; i < a_vector.size(); ++i) {
  weighted_sum += (i + 1) * a_vector[i];  // Just writing something a little nontrivial.
}

чем использовать конструкцию for_each или пытаться вписать это в вызов для накопления?

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

В любом случае разница невелика. По моему опыту, более 90% кода, который вы пишете, не критичен по производительности, но критично ко времени кодирования . Сохраняя ваш цикл STL буквально встроенным, он очень читабелен. Меньше косвенности, чтобы споткнуться, для себя или будущих сопровождающих. Если это в вашем руководстве по стилю, то вы экономите некоторое время на обучение для своих кодеров (признайте, что обучение правильному использованию STL в первый раз требует нескольких моментов). Этот последний бит - это то, что я имею в виду под ценой возможности записи.

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

2 голосов
/ 25 сентября 2008

Я бы не использовал жесткое и быстрое правило для этого. Есть много факторов, которые следует учитывать, например, часто вы выполняете ту или иную операцию в своем коде, просто цикл или «реальный» алгоритм, зависит ли алгоритм от большого количества контекста, который вам придется передавать в вашу функцию?

Например, я бы не стал ставить что-то вроде

for (int i = 0; i < some_vector.size(); i++)
    if (some_vector[i] == NULL) some_other_vector[i]++;

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

Есть много других примеров, где использование алгоритмов STL имеет большой смысл, но вы должны принимать решение в каждом конкретном случае.

1 голос
/ 02 октября 2009

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

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

OTOH, стоит учитывать тот факт, что многие программисты уже давно используют C ++ (а до этого - C, Pascal и т. Д.). Старые привычки тяжело умирают, и есть такая вещь, называемая когнитивный диссонанс , которая часто приводит к оправданиям и рационализации. Однако не спешите с выводами - по крайней мере, так же вероятно, что парни из стандартов виновны в диссонансе после принятия решения.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...