Чи заперечує цикл '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.