Чи заперечує цикл 'for' багато простих алгоритмів?


81

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

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

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

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

Чому я хотів би використовувати більш детальний опис std::generateна основі діапазону циклів for в C ++ 11?


14
Складаність? О, нічого, алгоритми з ітераторами в будь-якому випадку часто не складаються ... :(
Р. Мартіньо Фернандес

2
... які не є begin()і end()?
the_mandrill

6
@jrok Я сподіваюся, що багато людей вже мають певну rangeфункцію у своєму наборі інструментів. (тобто 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);. Немає жодної причини, що ви не можете мати як коротший, так і більш розбірливий код у добре побудованій бібліотеці.
Nicol Bolas

9
Це все в очах читача. Версія циклу зрозуміла для більшості фонів програмування: додайте значення rand до кожного елемента колекції. Std :: generate вимагає знання останнього C ++, або вгадування того, що генерування насправді означає "модифікувати елементи", а не "повернути згенеровані значення".
Хайд,

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()є більш нудним.
Lie Ryan

42

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


30

Особисто я прочитав:

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

є "ми призначаємо все в діапазоні. Діапазон є numbers. Присвоєні значення є випадковими".

Моє початкове читання:

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

"ми робимо щось для всього в діапазоні. Діапазон є numbers. Що ми робимо, це присвоюємо випадкове значення."

Це дуже чорно схожі, але не однакові. Однією з правдоподібних причин, яку я міг би викликати до першого читання, є те, що я думаю, що найважливішим фактом цього коду є те, що він присвоює діапазон. Тож є ваше "чому я хотів би ...". Я використовую, generateтому що в C ++ std::generateозначає "присвоєння діапазону". Як і до речі std::copy, різниця між ними полягає в тому, з чого ви призначаєте.

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

На відміну від цього, int&у вашому діапазоні циклу for є зморшка. Що робити, якщо тип значення діапазону не є int, тоді ми робимо щось надокучливо тонке, що залежить від того, наскільки він конвертований int&, тоді як generateкод залежить лише від повернення від randприсвоєння елементу. Навіть якщо тип значення такий int, я все одно можу зупинитися, щоб подумати, чи є це чи ні. Отже auto, що відкладає роздуми про типи, поки я не побачу, що їм присвоюють - auto &xя кажу "взяти посилання на елемент діапазону, який би тип не міг бути". Ще в C ++ 03, алгоритми (бо вони шаблони функцій) були способом приховати точні типи, тепер вони шлях.

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


Ви коли-небудь бачили користувацький тип із символом operator int&()? :)
fredoverflow

@FredOverflow замінити int&на, SomeClass&і тепер вам доведеться турбуватися про оператори перетворення та конструктори з одним параметром, не позначені explicit.
TemplateRex

@FredOverflow: не думаю. Ось чому, якщо це коли-небудь трапиться, я цього не чекатиму, і яким би я параноїком я ніколи про це зараз, це мене вкусить, якщо я не згадаю про це тоді ;-) Проксі-об'єкт міг би працювати, перевантажуючи operator int&()і operator int const &() const, але знову ж таки це може працювати, перевантажуючи operator int() constі operator=(int).
Steve Jessop

1
@rhalbersma: Я не думаю, що вам доведеться турбуватися про конструктори, оскільки non-const ref не прив'язується до тимчасового. Це лише оператори перетворення на посилальні типи.
Steve Jessop

23

На мій погляд, Ефективний пункт 43 STL: "Віддайте перевагу викликам алгоритму рукописним циклам". все ще є гарною порадою.

Зазвичай я пишу функції обгортки, щоб позбутися begin()/ end()hell. Якщо ви зробите це, ваш приклад буде виглядати так:

my_util::generate(numbers, rand);

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


Сказавши це, я повинен визнати, що в 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 для обгортки. Роблячи те саме. Мав бути в стандарті з 1-го дня (а може, 2 ...)
Макке

22

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

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

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

Намір стає набагато чіткішим, коли ви використовуєте std::generate.

1. ініціалізація в цьому контексті означає надання значущого значення елементам контейнера.


5
Хіба це не просто тому, що ви не звикли використовувати цикли на основі діапазону? Мені здається досить зрозумілим, що це твердження призначається кожному елементу в діапазоні. Зрозуміло, що генерація виконує те саме, що і ви знайомі std::generate, що можна припустити програмістом на C ++ (якщо вони не знайомі, вони шукатимуть, той самий результат).
Steve Jessop

4
@SteveJessop: Ця відповідь не відрізняється від інших двох. Це вимагає трохи більше зусиль від читача і дещо більше схильне до помилок (що, якщо ви забудете один &символ?) Перевага алгоритмів полягає в тому, що вони демонструють намір, тоді як з циклами ви повинні це зробити. Якщо у реалізації циклу є помилка, незрозуміло, чи є вона помилкою чи навмисною.
Девід Родрігес - dribeas

1
@ DavidRodríguez-dribeas: ця відповідь суттєво відрізняється від інших двох, IMO. Він намагається з’ясувати причину того, що автор вважає один фрагмент коду більш чітким / зрозумілим, ніж інший. Інші заявляють це без аналізу. Ось чому я вважаю цей досить цікавим, щоб відповісти на нього :-)
Стів Джессоп,

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);

Алгоритми на основі ітераторів дають вам кращий контроль за діапазоном, на якому ви працюєте.


8
Хоча причина читабельності є ВЕЛИЧЕЗНОЮ, яка віддає перевагу алгоритмам, це єдина відповідь, яка показує зациклений цикл є лише підмножиною того, що таке алгоритми , і, отже, не може нічого знецінити ...
K-ballo

6

Що стосується конкретного випадку std::generate, я погоджуюся з попередніми відповідями щодо питання читабельності / наміру. std :: generated мені здається більш зрозумілою версією. Але я визнаю, що це певним чином питання смаку.

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

Найпростішим прикладом буде std::fill. Загальна версія реалізована як цикл for через наданий діапазон, і ця версія буде використана під час створення шаблону шаблону. Але не завжди. Наприклад, якщо ви надасте йому діапазон, який є std::vector<int>- часто він насправді буде дзвонити memsetпід капотом, даючи набагато швидший і кращий код.

Тож я намагаюся розіграти тут карту ефективності.

Ваш рукописний цикл може бути таким же швидким, як версія алгоритму std ::, але навряд чи він може бути швидшим. Більше того, алгоритм std :: може бути спеціалізованим для певних контейнерів та типів, і це робиться в чистому інтерфейсі 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, то я б стверджував, що ні, алгоритми не будуть застарілими на основі діапазону для. У стандартному комітеті С ++ існує дослідницька група, яка працює над пропозицією додати діапазони до С ++, а також проводиться робота над поліморфними лямбдами. Діапазони позбавлять потреби використовувати пару ітераторів, а поліморфна лямбда дозволить вам не вказувати точний тип аргументу лямбда. Це означає, що це std::for_eachможна використовувати так (не сприймайте це як важкий факт, це просто те, як сьогодні виглядають мрії):

std::for_each(c.range(), [](x)
{
    // do stuff with x
});

Отже, в останньому випадку перевага алгоритму полягала б у тому, що, записуючи []лямбду, ви вказуєте нульове захоплення? Тобто, у порівнянні із просто написанням тіла циклу ви виділили шматок коду із контексту пошуку змінної, в якому він лексично відображається. Ізоляція, як правило, корисна для читача, менше думає про це під час читання.
Steve Jessop

1
Захоплення не в цьому суть. Справа в тому, що з поліморфною лямбдою вам не потрібно буде чітко вказувати, який тип x.
sdkljhdf hda

1
У цьому випадку мені здається, що в цьому гіпотетичному C ++ 1y for_eachвсе ще безглуздо, навіть коли використовується з лямбда-зондом. foreach + захоплення лямбда в даний час є багатослівним способом написання циклу, заснованого на діапазоні, і він стає дещо менш детальним, але все-таки більше, ніж цикл. Не те, що я думаю, що вам доведеться захищатись for_each, звичайно, але ще до того, як побачив вашу відповідь, я думав, що якщо запитувач хотів бити алгоритми, він міг би вибрати for_eachяк найм'якшу з усіх можливих цілей ;-)
Стів Джессоп

Не збирається захищатись for_each, але у нього є одна крихітна перевага перед діапазоном - ви можете зробити його паралельнішим легше, просто додавши до нього паралельний паралель_, щоб перетворити його parallel_for_each(якщо ви використовуєте PPL і вважаєте, що це безпечно для потоку) . :-D
sdkljhdf hda

@lego Ваша "крихітна" перевага справді є "великою" перевагою, якщо узагальнити її до того, що реалізація std::algorithms прихована за їхнім інтерфейсом і може бути довільно складною (або довільно оптимізованою).
Крістіан Рау,

1

Одне, на що слід звернути увагу, це те, що алгоритм виражає, що зроблено, а не як.

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

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


1

For-loop на основі діапазону - це саме це. Поки звичайно стандарт не буде змінений.

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

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