Как-то раз мне на глаза попалась статья о том, что самой дорогой ошибкой в дизайне языков программирования было решение определять окончание строки в C по NULL-байту. Один из вариантов перевода этой статьи на Хабре (хотя я, по-моему, читал другой). Эта статья меня немного удивила. Во-первых, как будто в те времена экономии каждого бита памяти можно было шикануть и выделить ещё 2-4 байта в каждой строке на хранение её размера. Во-вторых, никаких особо катастрофических последствий это решения для программиста не несёт. Ошибок, которые можно по этому поводу совершить я могу придумать целых две: неверно выделить память для строки (забыть место под NULL) и неверно записать строку (забыть NULL). О первой ошибке уже предупреждают компиляторы, избежать второй помогает использование библиотечных функций. Всей-то беды.

Значительно большей проблемой времён дизайна языка С (и затем С++) мне кажется другое — оператор for. При всей его кажущейся безвредности — это просто кладезь потенциальных ошибок и проблем.

Давайте вспомним классическое его применение:

for (int i = 0; i < vec.size(); i++)
{...}

Что же здесь может пойти не так?

1. for (int i = 0; i < vec.size(); i++)

Несмотря на то, что пример с int чаще всего идёт в учебниках на первых страницах, использование int чаще всего неверно. Мы, в основном, проходимся по массивам\векторам\спискам. Т.е. во-первых нам нужен беззнаковый тип, а во-вторых нам нужен тип данных, соответствующий максимальному размеру используемой коллекции. Т.е. правильно было бы написать

std::vector<int>::size_type

Скажите, часто вы такое писали? Вот именно. Это выглядит настолько страшно, что мало у кого хватало силы воли везде писать подобное. В итоге мы имеем миллионы неверно написанных циклов. Что это, если не ошибка в дизайне языка программирования?

2. for (int i = 0; i < vec.size(); i++)
Всех программистов учат грамотно именовать переменные. За имена вроде «a, b, temp, var, val, abra_kadabra» дают по рукам преподаватели на парах, ну или старшие коллеги молодым джуниорам. Однако, есть исключение. «Ну, если это счётчик в цикле, то можно просто i или j». Бр-р-р-р. Стоп! То есть давать корректные имена переменным нужно во всех случаях… кроме вот этих случаев, когда переменным по каким-то причинам понятные имена не требуются и можно написать одну непонятную букву? Это почему это так вышло? А вышло так потому, что если бы заставить программиста назвать переменную «currentRowIndex», то в цикле for её пришлось бы написать трижды:

for (int currentRowIndex = 0; currentRowIndex < vec.size(); currentRowIndex++)

В итоге длина строки вырастает с 37 до 79 символов, что неудобно ни читать, ни писать. Так что мы пишем i. Что приводит к тому, что во внутреннем цикле for мы уже используем j, в каком-нибудь алгоритме Флойда — Уоршелла Википедия рекомендует нам для третьего уровня цикла использовать переменную k и так далее. Кроме очевидной неочевидности написанного кода, мы здесь имеем ещё и ошибки копипасты. Возьмите напишите какое-нибудь перемножение матриц, с первого раза не перепутав нигде переменные i и j, каждая из которых в одном месте кода означает столбик, а в другом — строку матрицы.

Мы живём с этим из-за плохого дизайна цикла for.

3. for (int i = 0; i < vec.size(); i++)
Беда с циклом for в том, что как-правило, нам нужно начинать его просмотр с нулевого элемента. Кроме тех случаев, когда нужно с первого, второго, найденного ранее, последнего, закешированного и т.д. Набитая рука программиста привычно копипастит пишет = 0, а дальше требуется отладка и вспоминание кузькиной матери, чтобы исправить такое привычное = 0 на нужный вариант. Вы скажете, что вины for здесь нет, а есть невнимательность программиста? Я не соглашусь. Если того же программиста попросить написать тот же код с помощью do\while или while — он его напишет с первого раза без ошибки. Потому, что у него перед глазами в данном случае не будет приевшегося шаблона, все циклы do\while или while достаточно уникальны, программист каждый раз думает, с чего начинается цикл и по какому критерию он останавливается. В дизайне цикла for эта необходимость думать иногда кажется лишней, из-за чего ею пренебрагают практически всегда.

4. for (int i = 0; i < vec.size(); i++)
Удобная особенность цикла for состоит в том, что переменная i создаётся в области видимости цикла и уничтожается при выходе из неё. Это, в общем, хорошо и иногда позволяет сэкономить память или как-то задействовать RAII. Но это совершенно не работает в тех случаях, когда нам нужно что-то найти в цикле и остановиться. Остановиться-то мы можем, но чтобы вернуть индекс найденного элемента — нам нужна дополнительная переменная. Или определение i до цикла. Лишняя переменная — это неоправданные затраты для тех случаев, когда ничего найдено не будет. Объявление i до цикла ломает стройность кода — первая секция for остаётся пустой, что заставляет читателя вдумываться в код выше, пытаясь понять то ли это ошибка, то ли так и было надо.

Возможно, это выглядит придиркой, но для меня циклу for не хватает возможности возможности вернуть значение индекса в случае досрочной остановки. Это могло бы выглядеть как какой-нибудь пост-блок (вроде else для цикла while), в котором было бы доступно последнее значение счётчика итераций. Или функция в духе GetLastError(), которая возвращала бы последнее значение переменной i на момент вызова break;

5. for (int i = 0; i < vec.size(); i++)
Проверка условия во втором блоке оператора for не выглядит логичной, поскольку на каждой итерации цикла (кроме первой) сначала будет выполняться инкремент счётчика (третий блок) затем проверка условия (второй блок). Проверка условия находится во втором блоке, чтобы подчеркнуть тот факт, что она будет выполняться при первой итерации цикла сразу после инициализации счётчика i — только при этом объяснении всё выглядит более-менее логично. В итоге мы получили цикл, синтаксис которого сконцентрирован на первой его итерации и плохо отражает происходящее на всех последующих (которых обычно в разы больше). Такой уж дизайн оператора for.

6. for (int i = 0; i < vec.size(); i++)
«Меньше». Или «меньше равно»? Или «не равно»? До ".size()" или до ".size() — 1"? Да, на эти вопросы легко найти ответ, но почему, скажите, эти вопросы вообще можно\нужно себе задавать? И как в тех редких случаях, когда нужно написать нестандартный вариант дать знать коллегам-программистам, что это не ошибка, а именно так ты и собирался написать?

7. for (int i = 0; i < vec.size(); i++)
Это вообще единственное место, где мы рассказываем циклу, по какой, собственно, коллекции собираемся ходить. Да и то, упоминаем мы её лишь в контексте размера. Вот, мол, столько-то шагов нужно сделать. При этом в самом цикле мы вполне можем ходить по вектору vec2, который, конечно же, по закону подлости, в дебаге будет иметь точно такую же длину, а в релизе обязательно другую, из-за чего мы обнаружим этот баг значительно позже того момента, когда нужно было это сделать.

8. for (int i = 0; i < vec.size(); i++)
Как люди только не придумывают обозначение количества элементов коллекции! Да, STL со своим size() достаточно консистентен, но другие библиотеки используеют и length(), и count(), и number() и totalSize() — и всё это в разных вариантах CamelCase и under_score стилей написания. В итоге для использования концепции «размер коллекции» нам приходится циклу for давать знание о реализации вот этой конкретной коллекции. А при изменении коллекции на другую — переписывать все for'ы.

9. for (int i = 0; i < vec.size(); i++)
Здесь у нас, конечно же, любымый холивар о префиксной и постфиксной форме инкремента. Хотите передраться с коллегой и потратить полдня на вспоминание стандарта языка и изучения результатов оптимизаций кода современными компиляторами — добро пожаловать в старый добрый тред "++i vs i++". Есть много разных мест (и Хабр — одно из них) где об этом можно всласть поговорить, но неужели же надо было таким местом делать третий блок оператора for, используемого тысячами в каждом первом проекте?

10. for (;;)
Здесь мы имеем тоже классический спор «Да это самый эффективный способ организации бесконечного цикла!» с «Выглядит мерзко, while(true) значительно выразительнее». Больше холиваров богу холиваров!

11. for (int i = 0; i++; i < vec.size())
Этот код компилируется. Некоторые компиляторы выдают warning, но никто не выдаёт ошибку. Перепутанные местами второй и третий блок не бросаются в глаза, поскольку там написаны все знакомы вещи — инкремент, проверка условия. Оператор for выглядит как какой-нибудь аппаратный разъём, в который штекер можно воткнуть и так, и вверх ногами, при этом работать он будет только в одном случае, а во втором — сгорит.

Значительная часть дальнейшей эволюции языков программирования выглядит как попытка исправить for. Языки более высокого уровня (а в последствии и С++) ввели оператор for_each. Стандартные библиотеки пополнились алгоритмами поиска и модификации коллекций. С++ ввёл ключевое слово auto — в основном дабы избавиться от необходимости писать дикие
std::vector<int>::iterator
в каждом цикле. Функциональные языки предложили заменить циклы рекурсией. Динамические языки предложили отказаться от указания типа в первом блоке. Каждый попытался как-то исправить ситуацию — а ведь можно было сразу спроектировать получше.
Поделиться с друзьями
-->

Комментарии (115)


  1. A-Stahl
    19.09.2016 12:50
    +23

    >а ведь можно было сразу спроектировать получше
    Ага, и зачем делали ламповые триоды? Нельзя сразу было сделать полупроводниковый диод/транзистор?
    Паровой двигатель? Убожество какое. Нельзя сразу было сделать что-то адекватное?
    Ртуть как средство от заворота кишок? Да они там вообще врачами были или палачами-садистами?

    P.S. А за статью спасибо — давно я так не улыбался. Красиво написали.


    1. eugzol
      19.09.2016 14:30
      +6

      Ну а чем хуже на восемь лет раньше появившийся «FOR i = 1 TO 100… NEXT i»?


      1. Idot
        19.09.2016 18:50
        +5

        Мне в C-шных циклах нравится их потрясающая гибкость! То что шаг цикла может быть любой, в том числе и динамически меняющийся и может менять свой знак!
        А ещё можно запихнуть несколько переменных, например: for ( int i, j; i*j<200; i++, j*=-1.5)


        1. eugzol
          19.09.2016 18:55

          Так вот и выглядит это всё для всех, кто не привык к эзотерике for-циклов C-подобных языков, весьма запутанно :)


        1. MacIn
          19.09.2016 20:39
          +2

          В «негибких» никто, ну просто вообще никто не мешает делать то же самое на while циклах (+-). А цикл for, как именно цикл со счетчиком, для людей «из внешнего мира» выглядит не гибко, а дико. Технопрон.


          1. surly
            20.09.2016 00:01
            +3

            А кто сказал, что слово «for» должно всегда обозначать именно цикл со счётчиком? В Си так решили обозначать любой цикл, в котором можно выделить, как составные части: выражение для инициализации, выражение для проверки условия продолжения цикла и выражение для перехода к следующей итерации — причём, любое из этих выражений может быть пустым.


          1. 3aicheg
            20.09.2016 16:24

            Только проблемы в этом, по-факту, никакой нет — люди «из внешнего мира» на C/C++ всё равно не смогут писать. Пока не освоятся, но тогда они уже перестанут быть людьми «из внешнего мира».


            1. MacIn
              20.09.2016 16:44

              Это жонглирование словами. «люди из внешнего мира» = «люди, пришедшие из внешнего мира». И я сказал как раз о процессе перехода. Не говоря уже о том, что человек может писать на десятке языков, включая С++. Если он — не основной, то тоже можно говорить о «внешнем мире».


              1. 3aicheg
                23.09.2016 08:12

                А для тёти Кати из колхоза «Заветы Моисея» вообще всё программирование выглядит дико, на любом языке, ага. Если уж оценивать «технопрон», то вопрос, который надо задать — насколько часто в применении данной языковой фишки продоложают ошибаться люди с опытом, использующие данный язык на постоянной основе. А не насколько сильно конфузятся абсолютные новички, у которых в Паскале было не так.


                1. MacIn
                  23.09.2016 17:07

                  А для тёти Кати из колхоза «Заветы Моисея» вообще всё программирование выглядит дико, на любом языке, ага.

                  Это глупое передергивание, потому что «тетя Катя» не разбирается в вопросе совершенно, а мы говорим о разбирающихся людях.

                  Если уж оценивать «технопрон», то вопрос, который надо задать — насколько часто в применении данной языковой фишки продоложают ошибаться люди с опытом, использующие данный язык на постоянной основе

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


                  1. 3aicheg
                    26.09.2016 03:35

                    Это глупое передергивание, потому что «тетя Катя» не разбирается в вопросе совершенно, а мы говорим о разбирающихся людях.

                    Вам выше пишут разбирающиеся люди, что циклы гибкие и универсальные. Тоже глупо передёргивают? Ваше настоящее имя, случайно, не д'Артаньян?

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

                    Это очевидный критерий, он не мой, я его не придумал, а позаимствовал, кажется, из статьи, написанной создателем языка Ruby в ответ Никлаусу Вирту. Вирт как-то решил ознакомиться с новыми веяниями в дизайне стильных-молодёжных языков, для ознакомления выбрал язык Руби и самую ебанутую книжку по языку Руби («Why's poignant guide»), по результатам чего разродился статьёй в духе «А что это вы говорите, что у Руби простой и понятный синтаксис, я вот, например, ничего не понял...» — «Простой и понятный не для „человека из внешнего мира“, а для человека, освоившего язык!» — ответил ему автор, и был совершенно прав. Вирт вот тоже разбирающийся человек, но в данном случае выступил в роли конкретной «тёти Кати».


                    1. MacIn
                      26.09.2016 16:11

                      Тоже глупо передёргивают?

                      Нет, это аргумент другого уровня. О том, что этот цикл необычен, но хорош. Ваш — передергивание.

                      Это очевидный критерий, он не мой, я его не придумал, а позаимствовал, кажется, из статьи, написанной создателем языка Ruby в ответ Никлаусу Вирту

                      Игра словами — «мой» или «позимствованный мной». Я и не утверждал, что вы его выдумали, когда сказал «ваш аргумент». Ваш — значит, примененный вами. А уж придуман они или заимствован — другое дело.

                      А что это вы говорите, что у Руби простой и понятный синтаксис, я вот, например, ничего не понял

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

                      ответил ему автор, и был совершенно прав

                      Один человек говорит «крокодил зеленый», другой ему «нет, ты неправ, он длинный». И вы говорите, что второй совершенно прав. Ну да, крокодил-то длинный, фиг оспоришь. Только разговор-то был о цвете. Вот и я говорил о сравнении с другими языками, об обучении. Бессмысленно оговаривать, что синтаксис понятен тому, кто его понял («синтаксис может освоить тот, кто освоил синтаксис»).


                      1. MacIn
                        26.09.2016 16:19

                        Обсуждать понятность можно только в контексте обучения, когда человек чего-то не понимал, а потом начинает понимать, и процесс понимания происходит легче или труднее. Вы не можете сказать, что для человека, который понимает синтаксис (обучен), синтаксис является непонятным.


                        1. 3aicheg
                          26.09.2016 17:30

                          Обсуждать понятность можно в разных контекстах. Например, лежит в колыбельке младенец с соской и гугукает. Показываем ему цикл на Паскале, показываем цикл на C++ с чудовищными итераторами, да хоть монаду на Хаскеле — он на всё одинаково гугукает.

                          Или вот переходит программист на С с Паскаля. Смотрит на сишный цикл, первая мысль, конечно, «чозанах?» Ему достаточно понять, что в С выражение для цикла более общее, не только инкремент/декремент на единицу, а, как выше написали «инициализация, проверка, переход». Всё. Ментальное усилие максимум на пару минут. Больше проблем с синтаксической конструкцией цикла у него никогда не возникнет (хотя, возможно, по паскалевской привычке он продолжит использовать исключительно один целочисленный счётчик с приращением в единицу). Есть другие всякие языковые конструкты, полно их, таких, что, вроде, уже который год язык используешь, а регулярно надо в справочник лезть, чтобы заново вспомнить делати работы этого извращения — вот они действительно плохо воспринимаются. Но циклы for однозначно не из их числа.

                          Похожие же непонятки возникнут и у программиста, переходящего с С на Паскаль: чозанах, а как сделать цикл по убыванию? DOWNTO, фигассе, технопрон. А чтобы был счётчик float и умножался на 0.1? Вообще нельзя???


                          1. MacIn
                            26.09.2016 18:52

                            Обсуждать понятность можно в разных контекстах. Например, лежит в колыбельке младенец с соской и гугукает. Показываем ему цикл на Паскале, показываем цикл на C++ с чудовищными итераторами, да хоть монаду на Хаскеле — он на всё одинаково гугукает.

                            Это та же самая «тетя Катя». Для несведущего человека все реализации непонятны, потому что он не знает никакой (хотя, здесь можно обсудить то, что какая-то вариация может иметь бытовые параллели) О том, что понимать можно по-разному, я уже писал выше:
                            термин «непонятный» может быть истолкован, минимум, двояко, как это видно из нашего раговора


                            Ему достаточно понять, что в С выражение для цикла более общее, не только инкремент/декремент на единицу, а, как выше написали «инициализация, проверка, переход». Всё.

                            Разумеется, здесь просто накладывается то, что подобные конструкции в других языках (как минимум, времени появления Си и такого синтаксиса) реализовывались подобием while, а также то, что for в громадном количестве случаев используется «традиционно», включая книгу КиР (хотя и с оговоркой, что это только частность).

                            Похожие же непонятки возникнут и у программиста, переходящего с С на Паскаль:

                            В принципе, да, но в таком случае, моя фраза, которая вас так припекла, может просто быть просто развернута наоборот — «цикл в Паскале для человек из мира Си — технопрон какой-то», и это будет нормально. Другое дело, что кроме Си и тех, на кого он повлиял (Java и сотоварищи) есть языки, более старые, в основном, которые в свою очередь повлияли на другие и создали другое семейство. Да тот же Паскаль, черт с ним. И то, что поклонники этих «миров» обоюдно друг друга не понимают, неудивительно. Непонятно, отчего вас так возмутила моя фраза. В существоваших на момент появления Си языках циклы for были или циклами со счетчиками, или циклами для работы с чем-то типа range, циклы с пред и постусловием были отделены. Здесь же мы имеем гибкую — бесспорно — конструкцию, при помощи которой можно реализовать циклы вида while. Читаемо ли (легко ли понимается) это? По-моему, это открытый вопрос. Да, мы можем так сделать, но здорово ли заменять специальную управляющую конструкцию, которая читается и понимается однозначно универсальной, построенной как расширение толкования цикла со счетчиком? Это сочетание «можно, но надо ли?» я и назвал технопроном, тут нет ничего обидного.

                            Мы можем все три базовых вида цикла — с пред, пост условиями и счетчиком выполнить на if'ах и goto. Можно, но нужно ли? В свое время структурное программирование было создано именно для того, чтобы иметь набор базовых конструкций, используемых не рефлексивно (т.е. многоцелевым образом), а по-своему, что облегчает чтение кода. Возможно, мне стоило выразиться более детально и менее экспрессивно.

                            DOWNTO, фигассе, технопрон. А чтобы был счётчик float и умножался на 0.1? Вообще нельзя???

                            Это, кстати, да, удивительно. Потому как в даже простом Бейсике можно было в те года выполнить итерации с произвольным шагом, втч дробным.


                            1. MacIn
                              26.09.2016 19:01

                              Стоит добавить, что сама идея сишного цикла for — замечательная, просто удивительно «совмещение». ПМСМ, было бы здорово, если для некоторых задач были бы свои управляющие конструкции — хочешь пройти по всем элементам коллекции — так ннннна for range — ведь сделали же. Хочешь с счетчиком — вот тебе for, хочешь с условием окончания и обязательной операцией в конце итерации, втч срабатывающую при continue — возьми какой-нибудь loop специальный и т.д.

                              Бритва Оккама здесь не работает — иначе бы все делали на if, goto, адресной и простой арифметике — не нужно по сути ни while, ни do while, ни switch, ни for. Тем не менее, под специфические задачи выделены специфические конструкции. Но не здесь.


        1. xxvy
          20.09.2016 16:24
          +1

          Я при общении с БД часто использую такую запись

          for (Query->First(); !Query->Eof; Query->Next())
          

          очень удобно внутри цикла делать «continue». Не надо вручную ->Next() вызывать


  1. mend0za
    19.09.2016 12:56
    +11

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


    1. От неверного выделения памяти компиляторы не спасают. Т.к. зачастую размер строки вычислимый, на стадии компиляции неизвестен.

    2. От забыть NULL многие библиотечные функции не спасают. У них нет средств проверить максимальный размер буфера под строку.

    3. Имеется третья проблема — переполнение буфера, где финальный NULL затирается. И это как раз проблема дизайна,

    В Си мы не можем положится на язык и стандартные библиотечные функции при работе со строками. И дополняем стандартные средства своими проверками и вычислением границ памяти на постоянной основе.


    1. Bonart
      19.09.2016 13:22

      дубль, просьба удалить


  1. Bonart
    19.09.2016 13:23
    +20

    1. NULL как маркер конца строки — вообще не ошибка, об этом прямо написано по вашей же ссылке.
    2. O(N) при определении длины строки и затирание NULL при переполнении буфера — явно мелочи, не стоящие вашего внимания.
    3. Почему в упрек циклу for родом из C ставится истерическая типизация из C++?
    4. Чем провинились переменные индексы, чьи обозначения абсолютно адекватны, привычны и вообще пришли из математики?
    5. Почему рассматриваются исключительно варианты использования, где проблема не в наличии цикла for, а в отсутствии foreach?
    6. Нельзя было сразу спроектировать получше. Авторам требовалась портабельная замена ассемблеру на машинах семейства PDP — они с блеском решили эту задачу. Не их вина, что последователи принялись забивать новым молотком шурупы.


    1. ServPonomarev
      19.09.2016 14:42
      -5

      «Чем провинились переменные индексы, чьи обозначения абсолютно адекватны, привычны и вообще пришли из математики?»

      Они пришли из Фотрана. Где имя переменной определяло её тип. a,b,c — символы; i,j,k — целые. Кто в теме, знают.


      1. Bonart
        19.09.2016 14:48
        +11

        А в фортран (FORmula TRANslator) они пришли откуда, как вы думаете?


        1. ServPonomarev
          20.09.2016 07:57
          -2

          Ну, давайте ещё вспомним древних шумеров. Которые и придумали некоторые из современных символов. И что?

          Этот пост для минусов. Я вообще поражаюсь деградации, постигшей хабр в последнее время. Грустное зрелище.


          1. MacIn
            20.09.2016 16:48
            +3

            При чем тут деградация? ijk — стандартные математические индексы, которые были примерно всегда. Не во времена шумеров, но до появления ЭВМ. А вы говорите, что их родоначальником был Фортран, что неверно. За это поставили минусы. В чем проблема-то?


  1. Alesh
    19.09.2016 13:44
    +3

    За беллетристику пять! Технически статья не актуально аж с С++11.


    1. poxu
      19.09.2016 14:42
      -2

      Как в С++11 пробежать по каждому второму элементу вектора?


      1. Alesh
        19.09.2016 15:54
        +6

        Ну как-то так

        #include <iostream>
        #include <vector>
        
        int main(int argc, char const *argv[]) {
          std::vector<int> v{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
        
          for (auto it = v.begin(); it < v.end(); it += 2)
            std::cout << *it << "\n";
          return 0;
        }
        

        А что?


        1. poxu
          19.09.2016 17:59
          -1

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


          Я думал может есть решение типа for_each с шагом и лямбдой в качестве аргумента, или может принято получать особый итератор, который пройдёт только по каждому второму элементу, а потом писать


          for (auto&& value : <источник_особого_итератора>)


          1. Alesh
            19.09.2016 18:10

            Ну как-то выходит, что большей части проблем, найденных автором, это решение не снимает.

            Хм, даже интересно стало. А какие проблемы из названных в статье вы видите в этом решении? Давайте разберем.


            1. poxu
              19.09.2016 18:22
              -1

              Проблемы 2, 3, 4, 5, 9, 10 какими были, такими и остались.


              1. Alesh
                19.09.2016 19:07

                Давайте разберем:

                • Я извиняюсь, но второй пункт откровенно высосан из пальца и вообще мало имеет отношения именно к проблемам дизайна цикла for. С таким же успехом можно написать
                  while (пока_я_не_устану_смотреть_на_огонь_воду_и_как_работают_другие_люди) {}
                  и жаловаться на плохой дизайн while из-за того что получилась слишком длинная строка
                • Третий из той же оперы, если вы знаете как ошибок подобного рода позволит избежать хороший дизайн операторов в языке — напишите, реально интересно.
                • С четвертым в целом согласен, else блок был бы в тему.
                • С пятым пунктом я вообще не понял в чем проблема или скорее, почему это проблема?) Во всех языках обычные циклы for имеют такую же логику происходящую из того, что они являются частным случаем цикла while. Автор предлагает сломать всем мозг?)
                • К чему вы привели девятый пункт честно говоря не понял вообще.
                • Десятый? Вы пробовали в моем примере менять местами 2 и 3 выражение в for и код был скомпилировать? Да ладно), напишите каким копилятором пробовали, это очень интересный случай.


                1. poxu
                  19.09.2016 19:33

                  Десятый пункт и правда не компилируется, проверил.


                  Что касается остальных — вы явно не считатете ничего, кроме четвёртого пункта проблемами. А в коментарии своём написали, что они технически неактуальны начиная с С++11. Считаю, что это не проблема и считаю, что эта проблема уже неактуальна, а раньше была актуальна — это же принципиально разные утверждения.


                  1. Alesh
                    19.09.2016 20:27
                    -1

                    На самом деле я с самого начала считал, что проблемы 2,3,5,9 высосаны из пальца) и не брал их в расчет. И не я один так решил судя по комментариям)


                    1. poxu
                      19.09.2016 21:28
                      +1

                      Ну, если бы вы об этом написали, то детали вашего мнения были бы понятны не только вам :)


                      1. Alesh
                        19.09.2016 21:38

                        Полагая если вы начали придераться к деталям, то с основными моментами вы согласны?)


                        1. poxu
                          20.09.2016 06:19

                          Обратите внимание, вы написали, что за беллетристику статье надо поставить пять, но технически она не актуальна, начиная с С++11. Когда выяснилось, что значительной части проблем, описанных автором, С++11 не снимает вы сказали, что эти проблемы высосаны из пальца.


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


                          Я лично не считаю разницу между утверждениями "я ставлю статье пять баллов" и "я не согласен с половиной пунктов статьи" незначительными деталями и думаю, что существенно помог вам сформулировать своё мнение, которое я, как видно по моей ремарке про особый итератор, разделяю не полностью.


                          1. Alesh
                            20.09.2016 11:33

                            Еще раз вам пишу, перечисленные выше пункты я не считаю проблемами относящимися к реализации цикла for. Как может быть проблемой цикла for невнимательность программиста? Или проблема с (длинными или короткими) названиями переменных? В беллетристики описание таких проблем наряду с техническими допустимо для красочности сюжета, в технической нет.
                            Поэтому говоря о С++11 я имел ввиду только то из списка, что хоть как-то относится к тиме цикла for. Теперь понятна моя точка зрения?


                            1. poxu
                              20.09.2016 14:32
                              -2

                              Ваша точка зрения стала понятна как только вы сказали, что вы считаете проблемы 2,3,5,9 высосанными из пальца :).


                1. webkumo
                  19.09.2016 21:45
                  +1

                  4ый пункт — не знаю, как в плюсах, а в Java можно цикл выбросить в отдельную функцию и сделать return из нужного места цикла.


                  1. Dark_Daiver
                    20.09.2016 10:42

                    В плюсах все так же. Единственное «но» — цикл может использовать очень много переменных из текущего контекста, передавать их будет не очень удобно.


                    1. Jebediah_Kerman
                      20.09.2016 16:25

                      Можно использовать лямду с захватом переменных по ссылке.


          1. Alesh
            19.09.2016 19:24
            -1

            или может принято получать особый итератор

            Так можно, но зачем? Это не даст ничего к читабельности кода. Только лишние затраты на создание особого range для особого итератора.


            1. poxu
              19.09.2016 21:34
              -1

              Если такой итератор с вашей точки зрения ничего не даёт к читабельности кода, то получается, что и обычный


              for(auto&& item : vector) {
                  cout << item;
              }

              ничего не даёт к читабельности по сравнению с


              for (auto it = vector.begin(); it < vector.end(); it++) {
                  cout << *it;
              }


              1. Alesh
                19.09.2016 21:42
                -1

                А теперь попробуйте выполнить ваше же условие — «пробежать по каждому второму элементу вектора», то есть сделать необычную итерацию с помощью for range цикла, который позволит пробежать по каждому второму элементу вектора? И сравним.


                1. poxu
                  20.09.2016 06:24
                  +1

                  Ну как-то вот так это могло бы выглядеть


                  for(auto&& item : for_each(vector, 2)) {
                      cout << item;
                  }

                  думаете это ничего не даёт к читабельности по сравнению с


                  for (auto it = vector.begin(); it < vector.end(); it += 2) {
                      cout << *it;
                  }


                  1. Antervis
                    20.09.2016 06:54
                    -1

                    хорошо, положим, у вас есть метод for_each, возвращающий диапазон и в качестве второго аргумента принимающий шаг. Теперь усложняем пример: надо итерироваться от второго эл-та вектора до предпоследнего.


                    1. poxu
                      20.09.2016 07:01
                      +1

                      Добавим ещё 2 аргумента в перегруженную версию метода. Или, лучше введём абстракцию среза коллекции. Как-то вот так.


                      for(auto&& item : vector.slice(1, -1).for_each(2)) {
                          cout << item;
                      }


                      1. Antervis
                        20.09.2016 07:52
                        -1

                        а теперь скажем надо итерироваться в обратном направлении


                        1. poxu
                          20.09.2016 08:10

                          Ну, не будем долго размышлять, сделаем отдельный reverse_for_each


                          for(auto&& item : vector.slice(1, -1).reverse_for_each(2)) {
                              cout << item;
                          }

                          хотя можно и так


                          for(auto&& item : vector.slice(1, -1).reverse().for_each(2)) {
                              cout << item;
                          }


                          1. Antervis
                            20.09.2016 08:25
                            -1

                            … и вот теперь эта концепция генерирует более сложный и многословный код чем через итераторы. А если нам в цикле нужен еще и индекс элемента…


                            1. poxu
                              20.09.2016 08:59
                              +1

                              На всякий случай приведу оба примера кода вместе ещё раз. Вы утверждаете, что


                              for(auto&& item : vector.slice(1, -1).reverse_for_each(2)) {
                                  cout << item;
                              }

                              более сложно и многословно, чем


                              for (auto it = vector.rbegin() + 1; it < vector.rend() - 1; it += 2) {
                                  cout << *it;
                              }

                              Мне кажется, что второй вариант неслабо так более читаемый и в нём сложнее сделать ошибку.


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


                              for (auto it = vector.begin() + 1; it < vector.end() -1; it += 2) {
                                  cout << *it;
                              }

                              не проще чем


                              for(auto&& item : vector.slice(1, -1).for_each(2)) {
                                  cout << item;
                              }

                              И вот по поводу того, что будет, если вам понадобится индекс элемента. Если вам понадобится индекс элемента вы же вернётесь к простому циклу for, со всеми его недостатками, так? Все плюсы от использования итераторов будут потеряны. Будет что-то типа такого наверное


                              for (int i = vector.size() - 1; i >= 0; i -=2) {
                                  cout << i << " " << vector[i];
                              }

                              А если использовать подход, который защищаю я, будет


                              for(auto&& item : vector.slice(1, -1).reverse_for_each_with_index(2)) {
                                  cout << item.index << " " << item.value;
                              }

                              В первом случае код пришлось переписать и следить, чтобы при этом не сделать новых ошибок. Во втором — только добавить индекс и всё.


                              1. Alesh
                                20.09.2016 11:57
                                +1

                                Кстати с интересом бы прочитал про юсекейс где при итерации вектора может понадобиться индекс элемента)


                                1. DarkEld3r
                                  20.09.2016 12:16

                                  Не готов на конкретные примеры кода ссылаться, но бывают случаи когда у нас есть несколько связанных векторов (с одинаковым размером). Итерируемся по одному из них и, в каких-то случаях, по индексу обращаемся ко второму. Ну или ищем конкретное значение, но интересен его индекс.


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


                                  Поискал по нашей кодовой базе boost::adaptors::indexed() — 22 вхождения. И да, мне тоже кажется, что с такими итераторами работать удобнее чем с "обычным" циклом for. Хотя бы потому, что по первой строчке уже понятно как будет происходить итерация.


                                  1. Alesh
                                    20.09.2016 12:43

                                    Возможно в таких ситуациях выгоднее было бы использовать set или map. Но согласитесь, это все таки экзотические кэйсы.
                                    Ну а вообще я хотел услышать ответ от человека написавшего

                                    И вот по поводу того, что будет, если вам понадобится индекс элемента. Если вам понадобится индекс элемента вы же вернётесь к простому циклу for, со всеми его недостатками, так? Все плюсы от использования итераторов будут потеряны.

                                    И не подозревающего про:
                                    auto index = std::distance(v.begin(), it)
                                    


                                    1. DarkEld3r
                                      20.09.2016 12:46

                                      С экзотичностью, пожалуй, согласен, но так получается, что цикл for по диапазону покрывает 90% случаев и остаётся как раз экзотика. (:


                                    1. hdfan2
                                      21.09.2016 07:45

                                      А какая О у distance для list? Или, скажем, map? Оно же в цикле вызывается.


                                      1. Alesh
                                        21.09.2016 08:02

                                        Эмм, distance между элементами неотсортированного контейнера, зачем это вам?)


                                        1. hdfan2
                                          21.09.2016 12:34

                                          Ну ведь вы, как я понял, предложили использовать distance в том случае, если нужен индекс элемента. Ну пусть не для map (это действительно довольно странный случай), но для list это вполне естественное желание.


                                          1. Antervis
                                            21.09.2016 13:09

                                            ну список в этом плане в принципе неудобен: там и distance(), и operator[], и size() выполняются за линейное время. Поэтому придется комбинировать использование итераторов и счетчика


                                          1. Alesh
                                            21.09.2016 19:14
                                            -1

                                            Это было не предложение, а пример того что если сильно надо, то в цикле с итераторами индекс получить можно. А так я просто не вижу применимости этому. Выше посмотрите, обсуждали есть ли юсекейс где при итерации вектора может понадобиться индекс элемента, ну и как бы за редкими экзотическими исключениями…
                                            А дистансе для map это вообще странная хотелка, она не дает ровным счетом ничего. Исходя из этого и то что в этот момент обсуждали тоже самое по вектору я и написал вам такой ответ.


                              1. Antervis
                                20.09.2016 13:35

                                если вы знакомы с stl вы должны понимать, что вот это:

                                for (auto &&item : vector.slice(1, -1).reverse_for_each_with_index(2))
                                

                                не вписывается в парадигмы с++. Потому что написание миллиона member функций сделает stl нерасширяемым. Может быть что-то вроде
                                for (auto &item : step(slice(reverse(vector),1,-1),2))
                                

                                В общем, ranges proposal примерно в том направлении и движется.

                                Вариант с итераторами может быть немного многословен, но он очень универсален. Вот сами гляньте, насколько незначительные изменения в коде между:
                                for (auto it = vector.begin(); it < vector.end(); it++) // A
                                for (auto it = vector.rbegin() + 1; it < vector.rend() - 1; it += 2) // B
                                

                                в сравнении с
                                for (auto &item : vector) // A
                                for (auto &item : step(slice(reverse(vector),1,-1),2)) // B
                                

                                И при этом не надо помнить тонну утилитарных функций.


                                1. poxu
                                  20.09.2016 14:56

                                  Итераторы мощная штука, но код для тривиальных случаев получается хуже. Оставить бы итераторы и добавить методов для тривиальных случаев, но действительно в stl не впишется.


                                  Я слышал был какой-то proposal для extension methods, мне кажется тут это было бы полезно. Можно было бы добавить методов к объектам, просто подключив библиотеку.


                                  1. Sirikid
                                    20.09.2016 17:12

                                    > Я слышал был какой-то proposal для extension methods, мне кажется тут это было бы полезно.
                                    Предлагали ввести uniform function call syntax: https://en.wikipedia.org/wiki/Uniform_Function_Call_Syntax


                                1. DarkEld3r
                                  20.09.2016 15:09
                                  +1

                                  Потому что написание миллиона member функций сделает stl нерасширяемым.

                                  Дело постепенно движется в сторону принятия UCS и тогда это перестанет быть проблемой.


                                  И при этом не надо помнить тонну утилитарных функций.

                                  Опять же, с UCS IDE будет подсказывать доступные варианты. Плюс названия у таких функций, как правило, говорящие, ну или гугл подскажет по описанию того, что сделать хочется.


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


                        1. Tishka17
                          20.09.2016 08:13
                          +1

                          В python можно так:

                          for item in vector[::-1]:
                             pass
                          


                          1. poxu
                            20.09.2016 08:24

                            На всякий случай хочу уточнить. Это же итерация от первого до предпоследнего?


                            1. Tishka17
                              20.09.2016 08:29
                              +1

                              Нет. Это отрицательный шаг. В общем синтаксис такой vector[from:to:step]. Любую часть можно опустить.


                  1. Alesh
                    20.09.2016 11:38
                    -2

                    А ничё что при этом у вас создастся дополнительный объект с отфильтрованным набором?
                    Кстати внизу есть пост в примером использования библиотеки ranges v3. Так что вы изобретаете велосипед) А я вам показал просто способ с использованием голого С++.


                    1. poxu
                      20.09.2016 14:29
                      -1

                      Дополнительный объект конечно создаётся, но он не будет хранить отфильтрованный набор, он будет уметь отдавать итераторы на этот набор, а внутри будет ссылка на вектор.


                      Про библиотеку видел, даже прокоментировал, что она мне нравится.


                      1. Alesh
                        20.09.2016 15:04
                        -2

                        но он не будет хранить отфильтрованный набор, он будет уметь отдавать итераторы на этот набор

                        Пример осилите привести, или все таки признаете что мой цикл для подобных целей попроще будет?)


                        1. poxu
                          20.09.2016 15:12

                          Пример чего вы хотите увидеть. Чем примеры использования написанные моим гипотетическим псевдокодом и пример использования библиотеки ranges v3 вам не подошли?


                          1. Alesh
                            20.09.2016 18:17
                            -2

                            Понятно)
                            Просто я думал что мы какие-то реальные вещи обсуждаем, а не гипотетические.
                            И как следствие было интересно посмотреть как вы планировали в С++ реализовать генератор с состоянием, полагая что о нем идет речь в «он не будет хранить отфильтрованный набор, он будет уметь отдавать итераторы на этот набор».
                            Ну и как его собственно задействовать его потом в for.
                            Ну как говорится на нет и суда нет)


                            1. poxu
                              20.09.2016 18:31

                              Я на всякий случай повторюсь. Чем вас не устраивают исходники негипотетической библиотеки range-v3? :)


                              1. Alesh
                                20.09.2016 21:46
                                -2

                                Хотя сам ее не использую, хватает стандартных средств, но и что range-v3 не устраивает я как бы нигде не писал) Какая моя цитата навела вас на эту мысль?)

                                Я по моему даже указал на то что городить свои классы нет необходимости, потому что это уже реализовано для случаев если вдруг не хватает или не устраивают варианты на голом С++ (учитывая что stl уже в стандарте начиная с С++11)


                                1. poxu
                                  21.09.2016 13:50
                                  -1

                                  Ну как же, вы захотели примеров использования.


                                  Пример осилите привести, или все таки признаете что мой цикл для подобных целей попроще будет?)

                                  Я ответил, что есть мой псевдокод и есть примеры с range-v3.


                                  Вы захотели посмотреть как можно реализовать всё это не храня отфильтрованного набора данных.


                                  И как следствие было интересно посмотреть как вы планировали в С++ реализовать генератор с состоянием, полагая что о нем идет речь в «он не будет хранить отфильтрованный набор, он будет уметь отдавать итераторы на этот набор».

                                  Очевидно, что реализация есть в ranges-v3, поэтому я и спросил чем она вас не устраивает. Ну, то есть, почему не посмотреть, как это реализовано там?


        1. MacIn
          19.09.2016 20:49

          it < v.end();

          Есть ли технически разница с it != v.end()?


          1. encyclopedist
            19.09.2016 21:32

            Есть, не все типы итераторов можно сравнивать с помощью <. Так что предпочтительным в общем случае должно быть !=.


          1. Alesh
            19.09.2016 21:44

            Решение данной вводной «пробежать по каждому второму элементу вектора» без зацикливания возможно только с it < v.end().


            1. MacIn
              19.09.2016 22:19
              +1

              Это, кстати, вполне себе design flaw. Если использовать контейнер с непосредственным доступом и ограничением по номеру элемента вида " < v.count()", это будет работать одинаково для любых проходов.

              Мне не совсем понятня механика в данном случае: насколько я понимаю STL, .end() — особый вид итератора, который невалиден и располагается за последним элементом. Мы предполагаем здесь, что при двойной инкрементации мы можем попасть в зацикливание. Что происходит при инкрементации итератора .end()?

              calling this function might result in undefined behavior because calling operator ++ for the end of a sequence is not defined

              Ага, ну тогда ясно. Непонятно, почему не сделать так, что инкремент .end()'а давал бы тот же .end().


              1. Alesh
                20.09.2016 01:00

                Ага, ну тогда ясно. Непонятно, почему не сделать так, что инкремент .end()'а давал бы тот же .end().

                Можно код глянуть, но влом чета) Думаю просто не стали заморачиваться, итераторы на стандартных контейнерах сводятся к чистой арифметики указателей. А почему it < v.end() design flaw совсем не понял, по моему все логично — меньше последнего адреса, значит не вышло за границу range.


                1. MacIn
                  20.09.2016 04:40

                  Можно код глянуть, но влом чета) Думаю просто не стали заморачиваться, итераторы на стандартных контейнерах сводятся к чистой арифметики указателей.

                  Нет, просто по стандарту, насколько я понял, итератор должен позволять делать dereference, чтобы выполнять операцию инкремента. Это понятно — если у нас, скажем, линейный список, нам нужно, чтобы элемент дереференсился, чтобы найти адрес следующего. Но .end специальный вид итератора, для которого можно было (по идее, я могу ошибаться, мне было бы интересно, почему) определить операцию инкремента как всегда выдающую сам .end, потому что нет разницы, мы шагнули за пределы контейнера на 1 элемент, или на 100 — все равно выдаем .end, как признак выхода за пределы.
                  А почему it < v.end() design flaw совсем не понял

                  Не "<" design flaw, а то, что двойной инкремент может привести к UB.
                  по моему все логично — меньше последнего адреса

                  Так .end не последний адрес, а заведомо невалидный, после последнего.


                  1. Alesh
                    20.09.2016 11:41

                    Не "<" design flaw, а то, что двойной инкремент может привести к UB.

                    А ну это да, здесь полностью согласен, можно было бы и прикрутить проверку выхода за границыи оставлять указатель на end.


      1. kosmos89
        19.09.2016 16:37
        +3

        Для таких «изотерических» вариантов и задумывалась старая версия for. А для обычного прохода по всем элементам используйте range-based for.


        1. S_A
          19.09.2016 18:04

          Вопрос просто зашибись :) интересный и не корректный слегка. порассуждаю. язык не есть доказуемая теорема, а всего лишь система символов, причём с недоказуемыми (и всегда истинными, если язык содержит арифметику, согласно теоремам Гёделя, утверждениями). касательно теории катастроф. основа теории катастроф это всё-таки моделирование (мягкое, жесткое, не суть). языки, оперирующие моделями есть (я когда-то писал про на то время сыроватую OpenModelica, есть Wolfram ещё). С другой стороны, задумайтесь, а не есть ли нейросети реализация моделей теории катастроф. я сейчас с мобильника пока пишу не могу вот прикинуть, какого класса дифуры решаются функциями нейросетей, но поведение (те же бифуркации в обывательском понимании) имеют место. а в целом у меня ощущение что императивные языки, унаследованные от машины Тьюринга, ввиду своей частичной рекурсивности (проблемы остановки), не эквивалентны моделям ни нейросетей, ни моделям теории катастроф. последние ближе к практике, но очевидно это более узкий класс алгоритмов (ибо конечны). и самый большой вопрос, это user input, собственно. слишком далеки мы даже с нечёткой логикой и другими ИИ-алгоритмами, и железной их реализацией от природных и физических процессов: наши машины символьные, и отражают физику насколько символы их отражают. да, ваннами с ДНК пытаются взламывать хэши… но это тоже символьный хак. подытожу — язык, который вам нужен — не символьный. и ради любопытства загляните на runthemodel.com


        1. S_A
          19.09.2016 18:06

          Извините, промахнулся с мобилы. Отвечал Вячеславу.


      1. encyclopedist
        19.09.2016 22:00
        +2

        Или, с помощью библиотеки, вот так:


        #include <range/v3/view.hpp>
        using namespace ranges;
        
        std::vector<...> v;
        
        for (auto &element: view::stride(v, 2)) {
            do_smth(element);
        }


        1. poxu
          20.09.2016 06:26

          И это, на мой взгляд, значительно лучше, чем предыдущий пример.


        1. DarkEld3r
          20.09.2016 11:39

          Одна проблема: это будет не каждый второй элемент, а первый и дальше уже каждый второй.


          1. encyclopedist
            20.09.2016 12:02
            +2

            for (auto &element: v | view::drop(1) | view::stride(2)) {
                do_smth(element);
            }


  1. Vjatcheslav3345
    19.09.2016 15:15
    +1

    Вопрос — я знаю о стремлении к конструированию надёжных языков программирования, например исходя из представлений о языке как о математической доказуемой теореме а имеются ли языки, построенные исходя из математических теории хаоса и теории катастроф — т. е. не рассчитывающие на надёжность отдельных языковых элементов и пытающиеся построить из ненадёжного строительного материала — надёжные языковые конструкции-программы?


  1. NeoCode
    19.09.2016 15:16
    +2

    Интересная статья, люблю такие обсуждения, хоть и не могу с большей частью доводов согласиться. Основные проблемы дизайна С++ — это инклуды и препроцессор, а также использование шаблонов для метапрограммирования. Еще можно отметить то, что имя массива эквивалентно адресу первого элемента массива, что делает массивы «не-объектами» из-за экономии лишнего символа & (унифицированное взятие адреса).
    А что касается цикла for, то тут интересно вот что. Существует два «фундаментальных» способа доступа к коллекциям — последовательный и произвольный. Для произвольного существует общепринятая операция — квадратные скобки (индексация массивов и позже доступ к элементам ассоциативного массива). А для последовательного доступа такой операции нет. Обычно делают какой-то интерфейс итератора с методами типа First(), Next(), IsEnd() и т.п. (причем во всех языках они называются по-разному и логика работы немного разная). А хотелось бы придумать что-то общепринятое и в виде операции, а не методов интерфейса. И вообще максимально упростить этот аспект языка.


    1. springimport
      19.09.2016 19:52

      А что с инклудами?


      1. NeoCode
        19.09.2016 21:21
        +1

        Отсутствие модульности, которую пытались ввести в C++17 но так и не ввели.
        Инклуды это древнее и крайне примитивное средство имитации модульности. Каждый инклуд полностью включается в файл перед компиляцией. Таким образом, время сборки проекта резко увеличивается по сравнению с другими языками, в которых модульность есть.
        Всякие precompiled headers это по сути костыли.
        Шаблоны также существуют только в инклудах, их невозможно экспортировать в бинарные библиотеки.


  1. VaalKIA
    19.09.2016 17:06

    А кроме for each ничего больше не придумывали толкового для перебора по нескольким индексам одим оператором?


  1. lorc
    19.09.2016 17:42
    +2

    Функциональные языки конечно позволяют заменить цикл рекурсией, но обычно они смотрят уровнем выше и выкидывают цикл/рекурсию вообще. Вместо них вводят такие примитивы как map, fold, filter и т.д.
    Потому что чаще всего программисту не надо обходить что-то в цикле. Ему надо преобразовать одни элементы контейнера в другие, найти агрегат, отфильтровать и т.д. Мы же не пишем каждый раз реализацию сортировки когда нам нужно отсортировать массив, правда? Так зачем мы каждый раз пишем реализацию фильтрации, например?


  1. LoadRunner
    19.09.2016 17:56
    +2

    1. Не знаю, кто первый начал и под кого подстроился, но если значение нашего индекса-счётчика будет участвовать в операциях, где важен знак, то int как раз будет верным выбором. Иначе приведение типов.
    2. Я не программист и высказываю своё личное мнение, но неужели при виде однобуквенных имён переменных, особенно i, j, k, нет первой мысли, что это счётчик цикла? Я бы назвал дурным тоном использовать эти имена где-либо, кроме цикла.
    3. Это невнимательность программиста.
    4. Надуманная проблема. Кто мешает объявлять переменную вне цикла, если нужно вынести значение счётчика за его пределы? О каких неоправданных затратах речь — не понимаю.
    5. Что не так с порядком блоков? Объявляем переменную счётчик, задаём ей стартовое значение, проверяем с контрольным значением. Если всё хорошо — выполняем тело цикла и в конце прохода по телу цикла изменение счётчика. Не в начале следующего шага, а в конце текущего.
    Это не дизайн плохой, а восприятие цикла неправильное.
    6. Вот из-за неверного понимания структуры цикла и появляются такие пункты.
    7. Эм… Жалко сколько-то там байт на переменную счётчик, в которую загоняем нужное контрольное значение до начала цикла? Вместо этого просим компилятор самому оптимизировать этот момент, чтобы программа на каждом шаге не занималась этими вычислениями? Оооок, мистер_это_цикл_плохой_а_я_хороший.
    7. В случае смены метода, логично использовать Поиск&Замену. Я не представляю себе редактора, в котором можно писать код и нельзя делать поиск с заменой.

    Кстати, я не опечатался. Да и форматирование какое-то странное — я не вижу выделения. Или у меня монитор такой, яркость и системные шрифты?


    1. eshirshov
      22.09.2016 05:51
      -1

      чтобы понять боль автора топика, нужно персонально написать (и отладить) ну хотя-бы пару тысяч операторов цикла…


      1. LoadRunner
        22.09.2016 09:04

        Это же каким должен быть проект, если там столько циклов, которые отлаживает один человек?


  1. oYASo
    19.09.2016 18:07
    -1

    C++ — это язык, который одну и ту же задачу позволяет решать десятком разных способов. Это вынос мозга для новичков и киллер-фича для гуру. Реплики типа «смотрите, в пайтоне даже отступы стандартизированы» в мире плюсов должны вызывать лишь снисходительную улыбку. Ровно как и статья выше.

    Вы можете использовать for с индексами, если знаете зачем (ну или если не знаете больше ничего другого). А можете пользоваться range-based for, и все вопросы из статьи отпадают. А можете пользоваться std::for_each, и все вопросы также отпадают. А можете пользоваться while(it != vec.end()). А можете for(const auto& it = vec.begin(); it != vec.end(); ++it). Легко еще несколько проходов по коллекции написать, и в этом вся прелесть.

    Язык развивается, и это прекрасно.


  1. daiver19
    19.09.2016 19:14
    +4

    2.

    . «Ну, если это счётчик в цикле, то можно просто i или j». Бр-р-р-р. Стоп! То есть давать корректные имена переменным нужно во всех случаях… кроме вот этих случаев, когда переменным по каким-то причинам понятные имена не требуются и можно написать одну непонятную букву?

    Чем меньше скоуп, тем короче может быть имя. Всем понятно, что и — это индекс. Когда индекс играет бОльшую роль, переменную переименовывают.

    3.
    Набитая рука программиста привычно копипастит пишет = 0, а дальше требуется отладка и вспоминание кузькиной матери, чтобы исправить такое привычное = 0 на нужный вариант.

    Никогда такого не было за годы программирования. Если вы не осознаете, что пишете, у вас проблемы.

    4.
    Это могло бы выглядеть как какой-нибудь пост-блок (вроде else для цикла while), в котором было бы доступно последнее значение счётчика итераций.

    Скажите, что бы вы вернули, например, отсюда: «for (int i = 0, j = 5; i < 3 && j > 2; i++, j--)»?

    5.
    Проверка условия во втором блоке оператора for не выглядит логичной, поскольку на каждой итерации цикла (кроме первой) сначала будет выполняться инкремент счётчика (третий блок) затем проверка условия (второй блок)

    Первый цикл — присвоение, проверка, инкремент. Последующие — проверка, инкремент. Надо просто понимать, что инкремент происходит как бы в конце блока. Так что тут просто дело вкуса.

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

    Опять же, у цикла for полно применений, и простая итерация по контейнеру на данный момент является НЕстандарнтым вариантом.

    7.
    Да, STL со своим size() достаточно консистентен, но другие библиотеки используеют

    Стоп, мы о дизайне языка или о самодеятельности программистов на нем?

    8.
    но неужели же надо было таким местом делать третий блок оператора for, используемого тысячами в каждом первом проекте?

    К сожалению, такова плата за гибкость. Третья часть блока for — просто выражение, поэтому должно следовать синтаксису языка.

    9.
    Здесь мы имеем тоже классический спор «Да это самый эффективный способ организации бесконечного цикла!» с «Выглядит мерзко, while(true) значительно выразительнее». Больше холиваров богу холиваров!

    Еще можно label: goto label; И что?

    10.
    Этот код компилируется. Некоторые компиляторы выдают warning, но никто не выдаёт ошибку.

    Потому что он корректен, не?

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


  1. red75prim
    19.09.2016 20:19
    -1

    for (int i = 0, j = 5; i < 3 && j > 2; i++, j--)

    А теперь ещё перегрузим operator ,(int, int), и будет совсем замечательно.


    1. Antervis
      20.09.2016 06:04
      -1

      для интов вроде как не перегрузится


      1. red75prim
        20.09.2016 08:24

        Да, оказывается для примитивных типов запятая не перегружается.


        Но бывают случаи, когда возможность перегрузки нужно учитывать. Недавно наткнулся на вопрос: зачем нужен void() в (foo, void(), bar) в какой-то библиотеке шаблонов. Оказывается, что согласно пункту 5.2.2/7 стандарта С++ void() никогда не является валидным аргументом функции, поэтому, согласно пункту 13.3.1.2/9 стандарта С++, в этом случае используется стандартная запятая.


        http://stackoverflow.com/questions/39514765/the-void-the-comma-operator-operator-and-the-impossible-overloading


        Правильный способ записать такой цикл в общем случае: for (...; ... ; i++, (void)j--)


        (В процессе захотел посмотреть упомянутые пункты стандарта, посмотрел, посмотрел, и решил поверить автору ответа на слово)


  1. MacIn
    19.09.2016 20:31
    +1

    Объявление i до цикла ломает стройность кода — первая секция for остаётся пустой, что заставляет читателя вдумываться в код выше, пытаясь понять то ли это ошибка, то ли так и было надо.

    Что-что?
    Кто мешает сделать
    void abc() {
      int i;
    ..
    ..
    ..
      for (i = 0; i <= 10; i++){
    }
    }
    


    Проверка условия находится во втором блоке, чтобы подчеркнуть тот факт, что она будет выполняться при первой итерации цикла сразу после инициализации счётчика i — только при этом объяснении всё выглядит более-менее логично

    Нет, не только. Это стандартное объявление в самых разных ЯП аж с Фортрана: начало, конец и приращение.


  1. geher
    19.09.2016 21:41

    Странный подход.
    Как мне представляется, не стоит искать что-то сакральное в цикле for.
    Конечно, данный инструмент, как и многое в C/C++, дает много возможностей «отстрелить себе ногу», но это обычная цена за беспрецедентную гибкость языка.
    Чтобы не возникало большинства проблем, описанных в статье, достаточно не бездумно шлепать привычный шаблон цикла с индексом, а понимать, как оно устроено.
    И устроено оно не просто, а очень просто.
    Первая секция — инициализация. И это не обязательно присвоение начального значения индекса. Это может быть создание экземпляров классов, вызов инициализирующих функций, открытие файлов и т.п.
    Вторая секция — условие продолжения, которое проверяется до цикла. Тут может проверяться совершенно любое условие, и совсем не обязательно это проверка индекса на достижение границы перебора.
    Третья секция — это финализация очередной итерации цикла. Тот тоже может быть что угодно, как и в первой секции. И, опять же, совсем не обязательно это будет увеличение или уменьшение счетчика цикла.
    Пожалуй, за недоработку авторам языка можно поставить только отсутствие еще одной секции — финализации всего цикла, где можно было бы выполнить соответствующие действия, закрывающие открытое и убивающие созданное в секции инициализации. Причем исключительно ради полноты картины, т.е. для уравновешивания секции иницализации со столь широкими возможностями.
    Ну еще можно было бы докопаться, что финализацию итерации цикла можно было сделать после тела цикла, но в именно такой компоновке тоже есть сермяжная правда, ибо она позволяет сразу увидеть, как устроен данный цикл, абстрагировавшисть от кода в теле цикла.

    Что же до переменных с «неправильными» однобуквенными именами, так это же индексы! Они же в математических выражениях, как правило, записываются именно этими самыми буквами, и вводятся именно в такой последовательности — i, j, k. Чем вам программирование так сильно отличается, что там должно быть не так?


  1. bmj
    19.09.2016 22:06

    все ошибки в циклах от невнимательности (даже в цикле от 1 до 10 у вас пункт 7 встречается 2 раза, поправьте)…


    1. tangro
      19.09.2016 22:06

      Поправил.


  1. semenyakinVS
    20.09.2016 02:04

    Каждый попытался как-то исправить ситуацию — а ведь можно было сразу спроектировать получше


    И как, например?


  1. Sirikid
    20.09.2016 04:52
    +1

    TL;DR
    for дизайнили для C а не для C++


  1. Antervis
    20.09.2016 05:42

    вот насчет выделения «лишних» байт под размер строки автор точно погорячился: во-первых, если хранить размер, то его считать каждый раз не надо (а проверка на i < size быстрее чем проверка на str[i] != '\0'), а во-вторых, теперь STL с++ вынуждена хранить и размер, и терминирующий ноль.

    Что до i — использование его в качестве счетчика было принято как отраслевая best-practice еще задолго до формирования гайдлайнов к именованию переменных. Переменные с длинными именами тоже не везде легко воспринимать.


  1. mmMike
    20.09.2016 06:14
    -1

    А не смущает, что вилька бутылька пишется без мягкого знака, а сол фасол с мягким?
    А в английском языке вообще полно неправильных глаголов.
    И все это невозможно понять (если не лезть в историю), а нужно просто запомнить.


  1. Archaim
    20.09.2016 10:20

    Мне кажется, что не стоит так неосторожно использовать слово NULL. Правильно: null-terminated (прописными буквами), NUL (как название символа ASCII), '\0'.

    А NULL — это константа, введённая для обозначения нулевого указателя. Она обычно определяется как #define NULL 0, но предназначена для замены ключевого слова null, которое есть во многих других языках. Иногда её определяют как #define NULL (void *)0.

    В C и C++ с этим вообще небольшая путаница. Дело в том, что литерал «0» при использовании в случае с указателями означает не числовое значение «0», а значение нулевого указателя на данной конкретной платформе. Это может быть и адрес 0xFFFFFF или ещё какой-нибудь другой. В C++11 есть ключевое слово nullptr, лучше использовать его вместо NULL.


  1. G-M-A-X
    20.09.2016 15:21

    >Возьмите напишите какое-нибудь перемножение матриц, с первого раза не перепутав нигде переменные i и j, каждая из которых в одном месте кода означает столбик, а в другом — строку матрицы.

    Вы в институте переменным тоже для матриц давали осмысленные имена при письме в тетради? :)

    >Если того же программиста попросить написать тот же код с помощью do\while или while — он его напишет с первого раза без ошибки.

    Если не нужен цикл for, то не нужно его и использовать. Для простых циклов / других случаев как раз и есть другие циклы.

    >Удобная особенность цикла for состоит в том, что переменная i создаётся в области видимости цикла и уничтожается при выходе из неё.

    В PHP не уничтожается.
    В PHP переменную можно объявить в любой момент.
    Да и просто можно сделать return i;

    5 пункт вообще бред. :)

    >«Меньше». Или «меньше равно»?

    Это обычные вопросы. Они касаются и обычных условий.
    Обычно из параметров понятно, должно ли там быть строгое условие и допущена ошибка.

    >Здесь у нас, конечно же, любымый холивар о префиксной и постфиксной форме инкремента.

    Этот холивар относится не к только к циклу.

    >for (int i = 0; i++; i < vec.size())

    А можно так:
    for (int i = vec.size(); i--;)

    >Функциональные языки предложили заменить циклы рекурсией.

    Хм, а у них хватить памяти раскрутить длинный цикл? :)


    1. monah_tuk
      21.09.2016 05:09

      Хм, а у них хватить памяти раскрутить длинный цикл? :)

      а функциональщина на существующие аппаратные платформы не сильно и ложится :) Думаю, что по итогу данная рекурсия превратиться в недрах в тот же цикл, возможно, даже, с частичной раскруткой (см: хвостовая рекурсия).


  1. monah_tuk
    21.09.2016 05:04

    Статья или была взята из криокамеры (да и тогда насущные проблемы были другими), либо просто графомания.


    1. и согласен и не согласен. Правило — для индексов массива — только unsigned типы, считаю не совсем верной практикой:


      ptr[-50] = ...;

      легальный код, так как раскрывается в


      *(ptr - 50) = ...;

      Ведь этот самый ptr может уже быть смещён относительно реального начала блока памяти. Тут может выстрелить сам факт наличия арифметики указателей, но по природе языка — это неизбежный факт.

      Да в случае с std::vector это не совсем верно, а vector<>::size_type как по мне — так явный перебор, тем паче, что поголовно это std::size_t.

      При любом раскладе: ошибки дазайна языка тут нет. А вот к STL прикопаться можно. К примеру, можно не использовать std::vector, а остановиться на какой-то другой реализации.


    2. Аналогично. В данном случае — вкусовщина. Ещё ни разу не было проблем, когда изучал чужой код и там использовались i, j как счётчики циклов, тут даже так: видишь, сразу понимаешь, что это счётчик. Проблем ноль.

      А вот длинная запись с длинными именами: кто мешает использовать волшебную новую строку?


      for (int currentRowIndex = 0;
       currentRowIndex < vec.size();
       currentRowIndex++)

      Возьмите напишите какое-нибудь перемножение матриц, с первого раза не перепутав нигде переменные i и j, каждая из которых в одном месте кода означает столбик, а в другом — строку матрицы.

      А вот, что забавно, видел, и каюсь, сам грешен, и перепутанные column и row, так что тоже не показатель.

    3. Если у программиста появится устоявшийся шаблон с do/while, его это так же не убережёт от ошибки как и инициализация нулём в for


    4. Реально придирка. Лично стараюсь минимизировать область видимости переменных. А если хочется чего-то инициализацией индексом, ну сделайте что-то вроде:


      auto idx = [&vec]() {
      for (int i = 0; i < vec.size(); ++i) {
          ...
          if (...)
              return i;
          ...
      }
      return -1; // а как вам сообщить о том, что нужное - не найдено?
      }();

      или воспользуйтесь алгоритмами, типа std::search_n (вкупе с лямбдой), которые вернут итератор, а по нему можно:


      • проверить, а найден ли элемент вообще
      • получить индекс, хотя бы так: it - vec.begin(), но более правильно с std::distance(vec.begin(), it)

    5. Чего это не логична? Если что, цикл не будет выполнен ни одного раза, если при входе в него проверка не выполнилась. Что произошло? А произошло то, что проверка выполнилась РАНЬШЕ блока выражений. Так что всё тут логично, а прикопаться можно и к столбу. Вообще, если представить цикл for через while, то логики ещё прибавляется:


      {
      int i = 0;
      while (i < vec.size()) {
          ...
          i++;
      }
      }

    6. Бррррррр… Да это вообще посыл в духе: индексация с нуля или единицы. Ни. о. чём.


    7. Так используйте алгоритмы или вариант for-each из C++11! Или ограничивайте области видимости переменных или… Короче, написать ерунду можно на чём угодно и как угодно.


    8. А при чём тут for и язык программирования вообще!? И смотрите предыдущий пункт.


    9. И снова: при чём тут for? Там может быть любое выражение. А если холивар на эту тему возник — остановите его профилировкой. Я ни разу не видел заметной разницы между i++ и ++i в части примитивных типов.


    10. Оптимизация делает их идентичными. Более того, в современных системах бесконечный цикл, обычно, ждёт какого-то события: эвента, условной переменной, данных в очереди, тем самым отдавая процессорное время другим частям системы. В таких условиях даже наличие различий в проверке условий занимает ничтожно малое количество времени. Может быть критичным для всяких спин-локов, но… смотрим первое предложение этого пункта.


    11. Не знаю как вам, а мне бросаются. И снова: при чём тут оператор for? Да, язык позволяет неявно кастить int в bool, временами это бьёт (enum class, к примеру, уже не имеют неявного каста в bool), но такое можно написать и в while и в if. Косяк языка? Не думаю, скорее — функциональная особенность.

    Собственно, видно, что если какие-то нарекания и были, то они были устранены эволюционно (алгоритмы STL) или частично-революционно (C++11 и последующие). Причём, по большей части решалась проблема удобства и унификации, а не какие-то надуманные проблемы for.