Віддаєте перевагу алгоритмам рукописних циклів?


10

Що з перерахованого нижче вам здається більш читабельним? Рукописний цикл:

for (std::vector<Foo>::const_iterator it = vec.begin(); it != vec.end(); ++it)
{
    bar.process(*it);
}

Або виклик алгоритму:

#include <algorithm>
#include <functional>

std::for_each(vec.begin(), vec.end(),
              std::bind1st(std::mem_fun_ref(&Bar::process), bar));

Цікаво, чи std::for_eachдійсно це варте, враховуючи такий простий приклад, вже потрібна стільки коду.

Які ваші думки з цього приводу?


2
Відповідь на C ++ є досить очевидною. Але в інших мовах обидві приблизно рівні (напр., Python: map(bar.process, vec)хоча карта побічних ефектів не рекомендується перераховувати, а перелік розуміння / вирази генератора рекомендується проводити над картою).

5
А тут ще BOOST_FOREACH...
Бенджамін Баньє

Отже, пункт в ефективній STL вас не переконав? stackoverflow.com/questions/135129/…
Робота

Відповіді:


18

Існує причина, що лямбда були введені, і це тому, що навіть Стандартний комітет визнає, що друга форма смокче. Скористайтеся першою формою, поки не отримаєте підтримку C ++ 0x та лямбда.


2
Це ж міркування можна використовувати для виправдання другої форми: Комітет зі стандартів визнав, що він настільки перевершує, що вони ввели лямбда, щоб зробити це ще краще. :-p (все-таки +1)
Конрад Рудольф

Я не думаю, що другий за це так погано. Але може бути і краще. Наприклад, ви навмисно ускладнили використання об'єкта Bar, не додавши оператора (). Використовуючи це, це значно спрощує точку виклику в циклі і робить код охайним.
Мартін Йорк

Примітка: підтримка лямбда додана через дві основні скарги. 1) Стандартна котельня, необхідна для передачі параметрів функторам. 2) Немає коду в точці виклику. Ваш код не задовольняє жодної з цих ситуацій, оскільки ви просто викликаєте метод. Отже, 1) немає додаткового коду для передачі параметрів 2) Код вже не знаходиться в точці виклику, тому лямбда не покращить можливість ремонту коду. Так що лямбда - це чудове доповнення. Це не гарний аргумент для них.
Мартін Йорк

9

Завжди використовуйте той варіант, який найкраще описує те, що ви маєте намір зробити. Це є

Для кожного елемента xв vec, робити bar.process(x).

Тепер розглянемо приклади:

std::for_each(vec.begin(), vec.end(),
              std::bind1st(std::mem_fun_ref(&Bar::process), bar));

У нас for_eachтеж є - yippeh . У нас є [begin; end)асортимент, над яким ми хочемо оперувати.

В принципі, алгоритм був набагато більш чітким і, таким чином, кращим перед будь-якою рукописною реалізацією. Але тоді ... Біндери? Мемфун? В основному C ++ internans про те, як влаштувати функцію члена? Для мого завдання я їх не хвилюю ! Ні я не хочу страждати від цього багатослівного, моторошного синтаксису.

Тепер інша можливість:

for (std::vector<Foo>::const_iterator it = vec.begin(); it != vec.end(); ++it)
{
    bar.process(*it);
}

Зрозуміло, це загальна модель розпізнавання, але ... створення ітераторів, петління, збільшення та відсилання. Це теж усі речі, які мені не цікаві , щоб виконати своє завдання.

Слід визнати, що це виглядає waay краще , ніж перше рішення (по крайней мере, контур тіла є гнучким і досить чітко), але все ж, це на самому ділі не що великий. Ми використаємо цю, якщо у нас не було кращої можливості, але, можливо, у нас є ...

Кращий спосіб?

Тепер повернемося до for_each. Чи не було б чудово буквально говорити for_each та бути гнучким у виконанні операції, яка також повинна бути виконана? На щастя, оскільки C ++ 0x лямбдас ми є

for_each(v.begin(), v.end(), [&](const Foo& x) { bar.process(x); })

Тепер, коли ми знайшли абстрактне, загальне рішення для багатьох суміжних ситуацій, варто зазначити, що в цьому конкретному випадку є абсолютний номер 1 улюбленого :

foreach(const Foo& x, vec) bar.process(x);

Це насправді не може стати набагато зрозумілішим за це. На щастя, C ++ 0x get - це аналогічний вбудований синтаксис !


2
Сказав, що "подібний синтаксис" є for (const Foo& x : vec) bar.process(x);, або використовуючи, const auto&якщо вам подобається.
Джон Перді

const auto&можливо? Не знав цього - чудова інформація!
Даріо

8

Тому що це так нечитабельно?

for (unsigned int i=0;i<vec.size();i++) {
{
    bar.process(vec[i]);
}

4
Вибачте, але чомусь там написано "цензуру", де я вважаю, що має бути ваш код.
Mateen Ulhaq

6
Ваш код попереджає компілятор, оскільки порівнювати intзі знаком a size_tнебезпечно. Також, наприклад, індексація не працює з кожним контейнером std::set.
fredoverflow

2
Цей код буде працювати лише для контейнерів з оператором індексації, яких небагато. Він невпинно зв’язує алгоритм - що робити, якщо карта <> підходить краще? Оригінал для циклу та for_each не вплине.
JBRWilkinson

2
І скільки разів ви розробляєте код для вектора, а потім вирішуєте поміняти його на карту? Ніби розробка для цього була пріоритетом мов.
Мартін Бекетт

2
Ітерація над колекціями за індексом не перешкоджає тому, що деякі колекції мають низьку ефективність щодо індексації, тоді як усі колекції ведуть себе добре при використанні ітератора.
невдалий

3

загалом, перша форма читається майже всім, хто знає, що таке цикл, незалежно від того, на якому фоні вони є.

також взагалі, другий зовсім не такий читабельний: досить легко зрозуміти, що робить for_each, але якщо ви ніколи не бачили, std::bind1st(std::mem_fun_refя можу уявити, що це важко зрозуміти.


2

Насправді навіть із C ++ 0x я не впевнений for_each, що отримає багато кохання.

for(Foo& foo: vec) { bar.process(foo); }

Є спосіб більш читабельним.

Єдине, що мені не подобається в алгоритмах (у С ++) - це їх міркування щодо ітераторів, які створюють дуже багатослівні твердження.


2

Якби ви написали смужку як функтор, то це було б набагато простіше:

// custom functor
class Bar
{    public: void operator()(Foo& value) { /* STUFF */ }
};


std::for_each(vec.begin(), vec.end(), Bar());

Тут код досить читабельний.


2
Це зовсім читабельно осторонь того, що ви щойно написали функтор.
Вінстон Еверт

Дивіться тут, чому я вважаю, що цей код гірший.
sbi

1

Я віддаю перевагу останній, тому що він акуратний і чистий. Це насправді частина багатьох інших мов, але в C ++, це частина бібліотеки. Це насправді не має значення.


0

Зараз це питання насправді не характерне для C ++, я б сказав.

Вся справа в тому, що передача якогось функтора в іншу функцію не призначена для заміни циклів .
Передбачається спростити певні закономірності, які стають дуже природними завдяки функціональному програмуванню, наприклад, відвідувач і спостерігач , просто назвіть два, які мені спадають на думку негайно.
У мовах, яким не вистачає функцій першого порядку (найкраще прикладом є Java), такі підходи завжди потребують реалізації певного інтерфейсу, який є досить словесним та зайвим.

Загальне використання, яке я бачу багато в інших мовах, було б:

someCollection.forEach(someUnaryFunctor);

Перевага цього полягає в тому, що вам не потрібно знати, як someCollectionреально реалізується чи що someUnaryFunctorробить. Все, що вам потрібно знати, - це те, що його forEachметод буде повторювати всі елементи колекції, передаючи їх заданій функції.

Якщо ви особисто можете отримати всю інформацію про структуру даних, яку ви хочете повторити, та всю інформацію про те, що ви хочете зробити на кожному кроці ітерації, то функціональний підхід є надмірним ускладненням речей, особливо з мови, де це, як видається, досить нудно.

Крім того, ви повинні мати на увазі, що функціональний підхід повільніший, оскільки у вас є багато дзвінків, які надходять за певну ціну, які у вас немає в циклі.


1
"Крім того, ви повинні мати на увазі, що функціональний підхід повільніший, тому що у вас є багато дзвінків, які надходять за певну ціну, які у вас немає в циклі". ? Ця заява не підтримується. Функціональний підхід не обов'язково повільніше, тим більше, що під капотом можуть бути оптимізації. І навіть якщо ви повністю зрозумієте свою петлю, вона, мабуть, буде виглядати більш чистою у своїй більш абстрактній формі.
Андрес Ф.

@Andres F: Отже, це виглядає чистіше? std::for_each(vec.begin(), vec.end(), std::bind1st(std::mem_fun_ref(&Bar::process), bar));І це повільніше під час виконання або час компіляції (якщо пощастить). Я не розумію, чому заміна структур управління потоком функціональними викликами покращує код. Про будь-яку альтернативу, представлену тут, можна читати.
back2dos

0

Я думаю, що проблема тут полягає в тому, що for_eachнасправді це не алгоритм, це просто інший (і зазвичай неповноцінний спосіб) написання рукописного циклу. з цього погляду це повинен бути найменш використаний стандартний алгоритм, і так, ви, ймовірно, також можете використовувати цикл. однак порада в назві все ще стоїть, оскільки існує декілька стандартних алгоритмів, пристосованих до більш конкретного використання.

Там, де є алгоритм, який більш чітко виконує те, що ви хочете, тоді так, вам слід віддати перевагу алгоритмам над рукописними петлями. Очевидними прикладами тут є сортування з sortабо stable_sortпошук за допомогою lower_bound, upper_boundабо equal_range, але у більшості з них є певна ситуація, в якій їх краще використовувати в руці з кодованим циклом.


0

Для цього невеликого фрагмента коду обидва для мене однаково читаються. Взагалі я знаходжу ручні петлі, схильні до помилок і вважаю за краще використовувати алгоритми, але це дійсно залежить від конкретної ситуації.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.