Уникнення для циклів в C ++. але чому?

Моя вчителька сьогодні виступила з промовою про те, як ви ніколи не повинні/рідко використовуєте для циклів C ++, а замість цього використовуйте алгоритми STL. Він також каже, що якщо мені потрібно використовувати цикл for, переконайтеся, що він використовує ітератори або цикл на основі діапазону.

Потім він перейшов до диса на звичайний цикл:

for (int i = 0; i < N; ++i) ...

кажучи, що це практично ніколи не можна використовувати.

Тепер я пишу якийсь код, і звичайний цикл - це просто заощадження життя. Він застосований у багатьох ситуаціях, він простий і короткий, добре відомий, всі відразу знають, що відбувається, і це виконує роботу досить швидко (це C ++, а не Python!). Крім того, це також дуже ясно, коли реалізуються алгоритми математики, оскільки ці алгоритми описані з індексом ... не з алгоритмом STL або ітератором. Таким чином, використання звичайного для циклу відповідності відповідає цьому стилю.

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

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

Чому я не повинен використовувати цю дивовижну конструкцію?

7
Хотіли відповісти, але потім зрозуміли, що це головним чином буде про перефразування того, що сказав Шон Батько в його дивовижному Обговорення C ++ приправи . Це стосується цієї теми на початку своєї розмови, але я заохочую вас дивитися все це. TL; DW: менш схильний до помилок і більш виразний для алгоритму std, ніж необроблений цикл, коли це можливо.
додано Автор JBL, джерело
Це може бути з тих самих причин, що для над діапазоном не схвалюється в Python. Я бачив багато дискусій з цього приводу.
додано Автор Carcigenicate, джерело
@Scheff, але, звичайно, для (double d: values) легше читати?
додано Автор user2079303, джерело
@Scheff: петлі з двома ітераторами часто є ознакою того, що вам може знадобитися std :: transform , якщо не більш конкретний алгоритм.
додано Автор MSalters, джерело
<�я кожний негайно знає що відбувається - Не обов'язково. Вся людина дійсно знає що ви циклу. Що петля виконує, що вимагає подальшої самоаналізу. Наприклад, перегляньте розділ std :: partition і "Можливе впровадження". Якщо хтось повинен був написати цю версію і не сказати вам, що він робив, чи не могли б ви зрозуміти, що це робиться? Однак виклик std :: partiton дає безпосередній підказку про те, що зроблено. Можливо, саме це вказує вчитель.
додано Автор PaulMcKenzie, джерело
Велику частину часу, коли хтось каже вам, що щось є "злим" або що ви ніколи не повинні його використовувати, вони занадто ліниві, щоб розповісти вам всю історію. Існує і завжди буде використання звичайного старого для петель
додано Автор user463035818, джерело
Мова йде про те, щоб бути загальним. Сировинний цикл працює тільки з певними структурами даних. Стандартні алгоритми будуть працювати з будь-яким ітератором, що має початковий і кінцевий ітератор, якщо ітератор підтримує роботу алгоритму ( std :: sort потребує ітератор довільного доступу, а std :: count потрібний тільки ітератор введення). Швидше за все, ваш приклад може бути зроблений так само, він просто потребує іншого рівня абстракції.
додано Автор NathanOliver, джерело
Цикл for - це інструмент. Ви повинні використовувати інструмент, який найкраще підходить для даного завдання. Не існує інструменту, що відповідає всім можливим завданням.
додано Автор Algirdas Preidžius, джерело
@MSalters Я не впевнений, що std :: transform є панацеєю для цього в будь-якому випадку. Я мав на увазі щось на зразок ітерації через вектор властивостей (за індексом) і підключення кожного до обробника сигналу, для якого з'єднання зберігається у відповідному векторі (з ідентичним індексом) або щось подібне ... Я старомодна - я дотримуйтеся індексу для петель (таємно). ;-)
додано Автор Scheff, джерело
Ви можете зробити це таємно, не сказавши своєму вчителю ...
додано Автор Scheff, джерело
@ user2079303 Yepp, петлі діапазону фантазії і прохолодно - я згоден. Занадто погано, якщо у вас є два вектори, щоб проходити одночасно. Я читав, що для цього є навіть цікавіші речі, але я ще не смію їх застосовувати у своїй продуктивній роботі ...
додано Автор Scheff, джерело
До речі. Особисто я знаю, що використання ітераторів може бути корисним для відриву з конкретного контейнера - особливо в шаблонах. Але я (також) не бачу, чому для (std :: vector :: iterator = std :: begin (значення); iter! = Std :: end (значення); ++ iter ) {} краще читати, ніж для (size_t i = 0, n = values.size (); i , але це IMHO смак.
додано Автор Scheff, джерело
Найкраще розмовляти з вчителем безпосередньо;)
додано Автор hellow, джерело
Те, що ви дійсно бажаєте, 99% часу, є цикл, що базується на діапазоні , який обробляє всю ігровість ітератора для вас.
додано Автор hegel5000, джерело
До речі, не слід уникати циклу, але слід уникати std :: vector a = {0,1,2,3}; для (auto it = a.begin (); it! = a.end (); it ++) `it ++ оператор ітератора STL і використовуйте ++ it , оскільки оператор < code> ++ it просто збільшує адресу пам'яті та повертає той самий об'єкт, але оператор it ++ повертає новий об'єкт iterator - це значно погіршує використання продуктивності та пам'яті стека. Скотт Мейерс Ефективне STL Щоб уникнути цієї проблеми помилково і зробити код більш читабельним, краще використовувати алгоритми або циклічні цикли.
додано Автор Victor Gubin, джерело
Ви можете використовувати auto як тип ітератора. Підхід, про який ви говорите, має сенс, коли читаєте чийсь код.
додано Автор Алексей Неудачин, джерело
Ваше запитання дійсно цікаве, але занадто широке для StackOverflow. До речі, я думаю, що концепція, яку ваш вчитель хотів отримати, полягає в тому, що STL має багато оптимізованих процесів, які вже інтегровані, і ви повинні намагатися використовувати їх, коли це можливо; перешкоджати використанню циклу для суперечливо, оскільки більшість алгоритмів STL використовують петлі для
додано Автор Gsk, джерело

7 Відповіді

Коли існує існуючий алгоритм std , майже завжди краще його використовувати. Пошук, сортування, видалення дублікатів, пошук максимального (або n-го найбільшого) і так далі є звичайними операціями і зводяться до однорядних. Ці інструменти в заголовку алгоритму великі, і вони, безумовно, повинні мати перевагу над "необробленими" циклами .

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

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

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

11
додано

При вгаданні, ваш вчитель не любить для для саме причин, через які ви його любите:

Він робить все.

Given std::vector values { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

Порівняти:

auДоisEven = [](int i) { return (i % 2) == 0; };
auДоval = *std::find_if(values.begin(), values.end(), isEven);
std::cout << val << "\n";

До

int val;
for (auДо&& value : values)
{
     if ((value % 2) == 0)
     {
         val = value;
         break;
     }
}
std::cout << val << "\n";

Ви негайно побачите яку операцію відбувається ( std :: find_if ), і важливі деталі є помітними (назва isEven є параметром)

10
додано
Я вважав, що голосування за повторне відкриття, але ця відповідь переконала мене, що дійсно дуже багато думок. Ви можете вбудувати isEven , роблячи версію алгоритму нечитабельною - принаймні нечитабельною порівняно з можливістю витягування циклу у функцію. Це може бути корисною порадою для обох випадків: якщо ви хочете findTheFirstEvenNumberIn (someVector) , то це приємно, якщо ви можете написати саме це. (Існує багато причин використовувати алгоритм , дуже глибокий технічний, який виходить далеко за межі читабельності - але важко підсумувати їх для правильної відповіді ...)
додано Автор Marco13, джерело
Замість цього у випадку, якщо value є сукупністю баз даних SQL-сервера, віддайте перевагу auto && .
додано Автор Bathsheba, джерело
;-)
додано Автор Bathsheba, джерело
@cmaster основний блок спотикання, з яким я стикаюся з поточним станом , не дозволяє вам складати операції. Пропозиція діапазонів вирішує це, вам просто потрібно зберегти панель інструментів з перегляду view
додано Автор Caleth, джерело
@cmaster і якщо ви дійсно маєте щось нове, пам'ятайте, що ніколи не мав бути exhastive списком алгоритмів.
додано Автор Caleth, джерело
@midor якщо використовується val , обидва з цих прикладів мають UB, якщо в значень немає парних номерів. Це навмисне
додано Автор Caleth, джерело
за винятком того, що ваш код посилається на ітератор, який може бути end() , і, таким чином, ваш код гірший за цикл у його поточній формі.
додано Автор midor, джерело
Це вірно для випадків, коли вже існує відповідний алгоритм std :: , який точно відповідає тому, що ви робите. Проте проблеми реального програмування часто мають невелику примху, що робить алгоритми std :: досить марними.
додано Автор cmaster, джерело
@Bathsheba ... у випадку, якщо вам знадобиться фільтрувати непарну базу даних сервера, звичайно;)
додано Автор Max Langhof, джерело

На додаток до уникнення можливих помилок, мінімізації змінних циклу та оптимізації конструювання циклів, використання стандартних алгоритмів має перевагу в тому, що <�комунікаційний намір <�більше>, ніж загальний цикл. На мою думку, це їхня головна перевага. Таким чином, він дотримується вислову: "Пишіть код для ваших читачів, а не для компілятора або просто щоб отримати правильну відповідь".

Наприклад, якщо ви бачите std :: rotate() , std :: count_if() , std :: transform() або < code> std :: find_if() , ви знаєте шаблон того, що відбувається негайно без необхідності читати код (якщо ви дізналися ваш заголовок алгоритму).

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

Додаткову інформацію про цю тему див. У напівпрославленому розмовах C ++ .

5
додано

Чому я не повинен використовувати цю дивовижну конструкцію?

Не обов'язково використовувати його завжди , оскільки у багатьох випадках існують альтернативи, які, можливо, є кращими.

Давайте розглянемо, що ви хочете ініціалізувати вектор (створений кимось іншим) до [1,2,3, ..., N]. Припустимо, що новачок вирішує використовувати дивовижний цикл:

// let there be
std::vector seq(N);

// our code
for (int i = 1; i <= N; i++)
    seq[i] = i;

Дуже лаконічний і дуже компактний. Окрім того, новачок зробив помилку і в результаті програма має невизначену поведінку. Хоча для нас очевидні ветерани C ++, для початківців не очевидно, що допустимі індекси вектора закінчуються на N - 1 . Що ще гірше, в C ++ вам не гарантовано отримати помилку про доступ до поганого індексу (якщо ви не використовуєте vector :: at , але це повільніше, тому навряд чи хтось його використовує. ).

Тепер припустимо, що новачок був старанним і використовував правильну форму

for (int i = 0; i < N; i++)
    seq[i] = i + 1;

Тепер це правильно. Yay! Але що, якщо хтось інший вирішить, що вектор не є доречним. Вони потребують списку:

// let there be
std::list seq(N);

О ні! Вони зламали нашу петлю! std :: list не має оператора [] .

Compare this to using std::iota

std::iota(std::begin(seq), std::end(seq), 1);

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


добре відомо, що всі знають, що відбувається

Є мови програмування з індексами на основі 1, а індекси на основі 0 - інтуїтивно зрозумілі для початківців-програмістів. Деякі початківці можуть бути обдурені, думаючи, що вони знають, що відбувається, коли вони цього не роблять.

Шлейфи на основі діапазонів також добре відомі. Минуло 7 років з того моменту, як вони були ознайомлені з мовою, а також багато інших мов, включаючи Java, Python, PHP, щоб назвати кілька популярних.


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

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

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

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

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

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

Стиль C ++ - це все про безпечні звички кодування . Люди завжди говорять, що легко зняти себе в ногу з C ++. Тим не менш, я вважаю, що в основному це відбувається тому, що люди мають справу з C ++, як ніби вони мають справу з Java або Python або якоюсь мовою, що гарантує всі види безпеки. У цих мовах, наприклад, елемент, що знаходиться поза діапазоном, викине виняток, який може бути захоплений розробником. У C ++ доступ до виходу за межі діапазону може викликати цілий ряд катастрофічних проблем, які включають помилку сегментації, порушення доступу та читання пам'яті сміття. Якщо ви використовуєте циклічні діапазони та алгоритми, це менш ймовірно. Ось про що йдеться.

2
додано

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

  • Start index. Should it be 0 or 1?
  • What is the right limit? <= n or < n?

Але все ж, ІМО, недостатньо для виконання такого позову проти цієї конструкції.

1
додано
Алгоритми STL можуть бути оптимізовані виробником компілятора і легше перекладати.
додано Автор Thomas Matthews, джерело
@MSalters Що стосується циклів з кроками, відмінними від + - 1 ?
додано Автор NiVeR, джерело
@MSalters Не впевнений, що ви саркастичні чи ні :)
додано Автор NiVeR, джерело
@MSalters Ви маєте рацію, але давайте порівняємо рівень уваги, необхідний для того, щоб не допустити помилки при написанні vs ! = N . Це те саме? Крім того, порівняйте ймовірність того, що один з них створить помилки. Це те ж саме? Мені це робить різницю.
додано Автор NiVeR, джерело
Принаймні моя відповідь є популярним коментарем: D
додано Автор NiVeR, джерело
@MaxLanghof: Знову прочитайте цей коментар - "якщо N має бути навіть ". Якщо ваш контейнер за зовнішнім обмеженням завжди має рівний розмір, наприклад, тому що він зберігає чергування реальних і уявних частин складного числа, то немає необхідності знаходити наступне парне ціле число. Напр. після FFT, вибираючи реальні частини FFT. І якщо розмір вашого контейнера непарний, за допомогою i + = 2 це вже трохи підозріло. А як щодо того, що не споряджений предмет? Це моя фундаментальна точка: петля для (i = 0; i! = N; ++ i) проста. Все інше вимагає додаткової уваги. Яка конкретна хитрість відбувається?
додано Автор MSalters, джерело
Праворуч, звичайно, ! = N . Не перевищуйте свої тести.
додано Автор MSalters, джерело
@NiVeR: "Не перевищуйте ваші тести" все ще зберігається. Якщо ви знаєте, що розмір кроку дорівнює 2 і N має бути рівним, то не перевищуйте ваші тести. Напр. для (int i = 0; i! = 20; i + = 2) - ясно, що 20 має бути рівним, оскільки це літерал. Як читач, який бачить <20 , я повинен піклуватися, де прихована зміна в i є такою, що виправдовує перевизначення.
додано Автор MSalters, джерело
@cmaster: "Якщо n виявляється негативним," є помилка, і я навіть не впевнений, що повинно бути правильним. Якщо наміром було цикл від 0 до n, правильним рішенням було б --i . Якщо метою було цикл від n до 0, правильним рішенням було б for (int i = n; i! = 0; ++ i) . Цикл над нульовими елементами, коли початок не дорівнює кінцю, не може бути правильним у моїй книзі. Крім того, n у цьому контексті - це кількість елементів, які за визначенням не є негативними.
додано Автор MSalters, джерело
@cmaster стверджує, що діапазон [0, n) з негативним n є безглуздий , який не збігається з порожнім.
додано Автор Caleth, джерело
@MSalters Слід розуміти, що діапазон [0, n) з від'ємним n порожнім (тобто не існує такого x , що < код> 0 <= x ). І це принаймні дивно , якщо код починає повторювати що-небудь , якщо даний діапазон явно порожній. Особливо, якщо значення, що повторюються, навіть не знаходяться в діапазоні [-n, 0] , тобто цикл перебирає позитивні індекси, які безумовно, не в діапазоні, незалежно від того, яке визначення діапазону ви використовуєте. Використання i! = N як тесту верхньої межі, таким чином, є порушенням правила найменшого сюрпризу. І на це яскравий, якщо ви запитаєте мене.
додано Автор cmaster, джерело
@Caleth Незалежно від того, чи є це безглуздим, він не містить позитивних цифр. Код, який починає працювати з позитивними числами, коли задається [0, n) з негативним n , мене дивує. І мені не подобається дивуватися кодом.
додано Автор cmaster, джерело
@MSalters Я не згоден. для (int i = 0; i дає правильну поведінку, коли n має бути від'ємним, для (int i = 0; i! = n; i ++) з радістю спробує повторити всі позитивні числа, викликаючи невизначену поведінку.
додано Автор cmaster, джерело
@MSalters Так що якщо я не знаю розмір заздалегідь, мені потрібно знайти наступне ціле число в першу чергу? Це навіть не близьке до читаності ...
додано Автор Max Langhof, джерело

В основному, це знижує ймовірність помилок розробника та додавання накладних витрат По-друге, ітератори є загальними, і для з ітераторами записується таким же чином для кожного типу контейнера.
Це важливо, коли контейнер може бути змінений Подумайте, наскільки добре ваш цикл, написаний для вектора, буде обробляти пов'язаний список.

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

1
додано
IT KPI C/С++ новым годом
IT KPI C/С++ новым годом
747 учасників

Чат обсуждения С/С++. - Вопросы "напишите за меня лабу" - это оффтоп. - Оффтоп, флуд, оскорбления и вбросы здесь не приняты. - За нарушение - предупреждение или mute на неделю. - За спам и рекламу - ban. Все чаты IT KPI: https://t.me/itkpi/1147