Как-то раз мне на глаза попалась статья о том, что самой дорогой ошибкой в дизайне языков программирования было решение определять окончание строки в C по NULL-байту. Один из вариантов перевода этой статьи на Хабре: habrahabr.ru/post/126566 (хотя я, по-моему, читал другой). Эта статья меня немного удивила. Во-первых, как-будто в те времена экономии каждого бита памяти можно было шикануть и выделить ещё 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 здесь нет, а есть невнимательность программиста? Я не соглашусь. Если того же программиста попросить написать тот же код с помощью dowhile или while — он его напишет с первого раза без ошибки. Потому, что у него перед глазами в данном случае не будет приевшегося шаблона, все циклы dowhile или while достаточно уникальны, программист каждый раз думает, с чего начинается цикл и по какому критерию он останавливается. В дизайне цикла for эта необходимость думать иногда кажется лишней, из-за чего ею пренебрагают практически всегда.
3. for (int i = 0; i < vec.size(); i++)
Удобная особенность цикла for состоит в том, что переменная i создаётся в области видимости цикла и уничтожается при выходе из неё. Это, в общем, хорошо и иногда позволяет сэкономить память или как-то задействовать RAII. Но это совершенно не работает в тех случаях, когда нам нужно что-то найти в цикле и остановиться. Остановиться-то мы можем, но чтобы вернуть индекс найденного элемента — нам нужна дополнительная переменная. Или определение i до цикла. Лишняя переменная — это неоправданные затраты для тех случаев, когда ничего найдено не будет. Объявление i до цикла ломает стройность кода — первая секция for остаётся пустой, что заставляет читателя вдумываться в код выше, пытаясь понять то ли это ошибка, то ли так и было надо.
Возможно, это выглядит придиркой, но для меня циклу for не хватает возможности возможности вернуть значение индекса в случае досрочной остановки. Это могло бы выглядеть как какой-нибудь пост-блок (вроде else для цикла while), в котором было бы доступно последнее значение счётчика итераций. Или функция в духе GetLastError(), которая возвращала бы последнее значение переменной i на момент вызова break;
4. for (int i = 0; i < vec.size(); i++)
Проверка условия во втором блоке оператора for не выглядит логичной, поскольку на каждой итерации цикла (кроме первой) сначала будет выполняться инкремент счётчика (третий блок) затем проверка условия (второй блок). Проверка условия находится во втором блоке, чтобы подчеркнуть тот факт, что она будет выполняться при первой итерации цикла сразу после инициализации счётчика i — только при этом объяснении всё выглядит более-менее логично. В итоге мы получили цикл, синтаксис которого сконцентрирован на первой его итерации и плохо отражает происходящее на всех последующих (которых обычно в разы больше). Такой уж дизайн оператора for.
5. for (int i = 0; i < vec.size(); i++)
«Меньше». Или «меньше равно»? Или «не равно»? До ".size()" или до ".size() — 1"? Да, на эти вопросы легко найти ответ, но почему, скажите, эти вопросы вообще можнонужно себе задавать? И как в тех редких случаях, когда нужно написать нестандартный вариант дать знать коллегам-программистам, что это не ошибка, а именно так ты и собирался написать?
6. for (int i = 0; i < vec.size(); i++)
Это вообще единственное место, где мы рассказываем циклу, по какой, собственно, коллекции собираемся ходить. Да и то, упоминаем мы её лишь в контексте размера. Вот, мол, столько-то шагов нужно сделать. При этом в самом цикле мы вполне можем ходить по вектору vec2, который, конечно же, по закону подлости, в дебаге будет иметь точно такую же длину, а в релизе обязательно другую, из-за чего мы обнаружим этот баг значительно позже того момента, когда нужно было это сделать.
7. for (int i = 0; i < vec.size(); i++)
Как люди только не придумывают обозначение количества элементов коллекции! Да, STL со своим size() достаточно консистентен, но другие библиотеки используеют и length(), и count(), и number() и totalSize() — и всё это в разных вариантах CamelCase и under_score стилей написания. В итоге для использования концепции «размер коллекции» нам приходится циклу for давать знание о реализации вот этой конкретной коллекции. А при изменении коллекции на другую — переписывать все for'ы.
8. for (int i = 0; i < vec.size(); i++)
Здесь у нас, конечно же, любымый холивар о префиксной и постфиксной форме инкремента. Хотите передраться с коллегой и потратить полдня на вспоминание стандарта языка и изучения результатов оптимизаций кода современными компиляторами — добро пожаловать в старый добрый тред "++i vs i++". Есть много разных мест (и Хабр — одно из них) где об этом можно всласть поговорить, но неужели же надо было таким местом делать третий блок оператора for, используемого тысячами в каждом первом проекте?
9. for (;;)
Здесь мы имеем тоже классический спор «Да это самый эффективный способ организации бесконечного цикла!» с «Выглядит мерзко, while(true) значительно выразительнее». Больше холиваров богу холиваров!
10. for (int i = 0; i++; i < vec.size())
Этот код компилируется. Некоторые компиляторы выдают warning, но никто не выдаёт ошибку. Перепутанные местами второй и третий блок не бросаются в глаза, поскольку там написаны все знакомы вещи — инкремент, проверка условия. Оператор for выглядит как какой-нибудь аппаратный разъём, в который штекер можно воткнуть и так, и вверх ногами, при этом работать он будет только в одном случае, а во втором — сгорит.
Значительная часть дальнейшей эволюции языков программирования выглядит как попытка исправить for. Языки более высокого уровня (а в последствии и С++) ввели оператор for_each. Стандартные библиотеки пополнились алгоритмами поиска и модификации коллекций. С++ ввёл ключевое слово auto — в основном дабы избавиться от необходимости писать дикие
std::vector<int>::iterator
в каждом цикле. Функциональные языки предложили заменить циклы рекурсией. Динамические языки предложили отказаться от указания типа в первом блоке. Каждый попытался как-то исправить ситуацию — а ведь можно было сразу спроектировать получше.
Автор: Инфопульс Украина