Перед вами продолжение серии статей, которую можно озаглавить "ужасы для программистов". В этот раз речь пойдёт о типовом паттерне опечаток, связанном с использованием чисел 0, 1, 2. Неважно, пишете вы на C, C++, C# или Java. Если вы используете константы 0, 1, 2, или если эти числа содержатся в именах переменных, то, скорее всего, Фредди заглянет к вам ночью в гости. Читайте и не говорите потом, что вас не предупреждали.
Введение
Я продолжаю серию статей, посвященных замеченным закономерностям в том, как люди допускают ошибки. Предыдущие публикации:
В этот раз закономерность заметил не я, а мой коллега Святослав Размыслов. Он обратил внимание, что постоянно в своих статьях описывает проблемы, в которых фигурируют переменные, содержащие в своём имени числа 1 и 2. Святослав предложил мне подробнее исследовать эту тему и она действительно оказалась весьма плодотворной. Выяснилось, что в нашей коллекции ошибок содержится большое число фрагментов кода, неправильных из-за того, что люди путаются в индексах 0, 1, 2 или в именах переменных, содержащих такие числа. Выявлена новая интересная закономерность, о которой и пойдёт речь ниже. Я благодарен Святославу за подсказку исследовать эту тему и посвящаю эту статью ему.
Святослав Размыслов, менеджер, внимательный ловец багов и просто талантливый человек.
Какова цель этой статьи? Показать, как легко все мы ошибаемся и допускаем опечатки. Если программисты будут предупреждены - они будут внимательнее в процессе обзоров кода, сосредотачивая своё внимание на злополучных 0, 1, 2. Также программисты лучше смогут ощутить ценность статических анализаторов кода, которые помогают заметить такие ошибки. Дело не в рекламе PVS-Studio (хотя в ней тоже :). До сих пор многие программисты считают статический анализ лишним, предпочитая концентрироваться на собственной аккуратности и обзорах кода. К сожалению, стараться писать код без ошибок - это хорошо, но недостаточно. Эта статья в очередной раз это ярко продемонстрирует.
Никто не застрахован от таких ошибок. Ниже вы увидите эпичные ляпы в даже таких известных проектах как Qt, Clang, Hive, LibreOffice, Linux Kernel, .NET Compiler Platform, XNU kernel, Mozilla Firefox. Причем это не какие-то экзотические редкие ошибки, а самые что ни на есть частые. Не убедил? Тогда приступим!
"Болтовня ничего не стоит! Покажите мне баги!"
(c) переделанная цитата Linus Torvalds.
Опечатки в константах при индексации массивов
Как правило, в наших статьях мы приводим предупреждения, с помощью которых найдены ошибки. В этот раз я опущу эти предупреждения, так как и без них ошибка будет легко заметна и понятна. Впрочем, хотя эти ошибки сразу бросаются в глаза в коротком фрагменте кода, они отлично умеют прятаться в коде проектов.
Начнём с ошибок, когда возникает путаница с числовыми литералами, используемыми для индексации массивов. Несмотря на банальность этих ошибок, их много и обнаруживаются они отнюдь не в лабораторных работах студентов.
Проект XNU kernel, язык C
Строчку скопировали, но поправить индекс забыли. Как я понимаю, здесь должно быть написано:
Проект LibreOffice, язык C++
Как и в предыдущем случае, строчку скопировали, но забыли исправить 0 на 1. Исправили только строковый литерал.
Можно задаться философским вопросом, как можно допустить такую ошибку в функции из четырех строк? Можно и всё. Вот такое оно, программирование.
Проект Quake-III-Arena, язык C
В скопированной строчке забыли заменить dir[1] на dir[2]. Как результат - значение по оси Z не контролируется.
Проект OpenCOLLADA, язык C++
Да, даже в таком коротком конструкторе можно умудриться выйти за границу массива при его инициализации.
Проект Godot Engine, язык C++
Комментария не требуется.
Проект Asterisk, язык C
При написании однотипных блоков, ошибка, как правило, располагается в нижележащем. До этого все рассмотренные случаи были как раз такими. Здесь же опечатка находится в необычном месте, а именно, в первом блоке. Почему так получилось, сказать сложно. Мне не остаётся ничего иного, как просто привести картинку с единорогом, пожимающим плечами:
Проект Open CASCADE Technology, язык C++
Два раза в одну и туже ячейку массива копируются разные значения. Явная ошибка, но как её исправить мне было непонятно, так как код проекта мне не знаком. Поэтому я просто посмотрел, как разработчики поправили код после того, как наша команда указала им на эту ошибку. Правильный вариант:
Проект Trans-Proteomic Pipeline, язык C++
Я переживаю, что пакеты для научных исследований содержат такие ошибки. Trans-Proteomic Pipeline предназначен для решения задач в сфере биологии. Можно такого нарешать и "наисследовать". В этом пакете вообще находилось много интересного: проверка 2012 года, проверка 2013 года. Пожалуй, можно вновь попробовать взглянуть на этот проект.
Проект ITK, язык C++
Перед нами другой проект для проведения исследований в области медицины: Medicine Insight Segmentation and Registration Toolkit (ITK). Проект другой, а ошибки те же самые.
Проект ITK, язык C++
Чистый Copy-Paste.
Проект ReactOS, язык C++
По всей видимости, константа crBackgnd должна записываться в ячейку cols[2].
Проект Coin3D, язык C++
Дважды проверятся элемент массива size[1], а элемент size[2] не проверятся. Вот так и появляются странные артефакты на изображениях.
Проект OpenCV, язык C++
Прям чувствуется, как выражение cmptlut[0] < 0 было дважды размножено копированием, но поправили ноль только в одном месте.
Проект Visualization Toolkit (VTK), язык C++
Здесь и далее я не буду комментировать многие подобные ошибки. Что тут комментировать? Главное, разглядывая подобные фрагменты кода, прочувствовать, что хотя ошибка простая, это не значит, что она будет замечена программистом.
Проект Visualization Toolkit (VTK), язык C++
Здесь программист явно торопился побыстрее написать код. По-другому сложно объяснить, как он ошибся трижды. Элементы массива вычитаются сами из себя. В результате получается, что этот код эквивалентен:
Впрочем, этот код можно сократить ещё больше:
Великолепно. В коде есть длинное серьёзно выглядящее выражение, которое, на самом деле, ничего не делает. Люблю такие случаи.
Проект Visualization Toolkit (VTK), язык C++
Аналогичный случай спешного написания кода.
Дважды повторяется сравнение newPos[2] != oldPos[2].
Проект ADAPTIVE Communication Environment (ACE), язык C++
В условии должно проверяться, что встречены две косые черты, стоящие после двоеточия. Другими словами, ищется подстрока "://". Из-за опечатки, проверка "подслеповата" и готова второй косой чертой посчитать любой символ.
Проект IPP Samples, язык C++
Опечатка кроется вот здесь, в передаваемых в макрос аргументах:
Получается, что выбирается минимум из двух одинаковых значений. На самом деле, должно быть написано:
Кстати, этот код может продемонстрировать пользу стандартной библиотеки. Если написать так:
То код станет короче, и меньше подвержен ошибкам. Собственно, чем меньше однотипного кода, тем больше шансов, что он будет написан правильно.
Проект Audacity, язык C++
Правильное выражение:
Проект PDFium, язык C++
Дублируется ряд действий по инициализации структуры. Те строчки, что помечены комментарием //2, можно удалить, и ничего не изменится. Я сомневался, включать ли этот фрагмент кода в статью. Здесь не совсем ошибка, и не совсем с индексами. Однако этот лишний код, скорее всего, появился именно из-за того, что программист запутался во всех этих членах класса и индексах 1, 2. Так что, думаю, этот фрагмент кода подходит для демонстрации, как легко путаться в числах.
Проект CMake, С
Рассматриваемый далее код написан не разработчиками CMake, а позаимствован. Судя по комментарию в начале файла, функция utf8_encode была написана Tim Kientzle ещё в 2007. С тех пор эта функция кочует из проекта в проект и много где встречается. Я не стал изучать вопрос первоисточника, так как это сейчас не принципиально. Раз этот код есть в проекте CMake, то и ошибка относится к CMake.
Как видите, есть какая-то путаница с индексами. Дважды происходит запись в элемент массива p[1]. Если изучить код по соседству, то становится понятно, что правильный код должен быть таким:
Примечание
Обратите внимание, что все ошибки, рассмотренные в этой главе, относятся к коду на языке C или C++. Нет кода на C# или Java!
Это очень интересно, я не ожидал подобного. На мой взгляд, рассмотренные опечатки никак не зависят от языка. И в следующих главах действительно появятся ошибки в коде на других языках. Думаю, это просто случайное совпадение. Анализатор PVS-Studio начал гораздо позже поддерживать языки C#/Java по сравнению с C/C++, и мы просто не успели накопить в базе соответствующее примеры ошибок.
Тем не менее, наблюдение всё равно интересное. Видимо, C и C++ программисты больше любят использовать числа 0, 1 и 2 при работе с массивами :).
Опечатки в именах
Это будет самый большой раздел. Людям очень легко запутаться в таких именах, как a1 и a2. Кажется, как тут можно запутаться? Можно. Легко. И сейчас читатель сможет в этом убедиться.
Проект Hive, язык Java
Функция сравнения compare принимает два объекта: o1 и o2. Но из-за опечатки далее используется только o2.
Что интересно, благодаря Copy-Paste эта ошибка мигрировала в другую функцию:
Проект Infer.NET, язык C#
Проект Doom 3, язык C++
Если кто-то сразу не заметил опечатку, то нужно посмотреть на строку, где инициализируется переменная l2. Следовало использовать exp2.
Проект Source Engine SDK, язык C++
Правильно:
Проект Linux Kernel, язык C
Кстати, опечатки могут быть не только в именах переменных, но и в именах макросов. Сейчас будет несколько таких примеров.
Как видите, дважды используется маска с именем BIT1, что делает вторую проверку бессмысленной. Тело второго, помеченного комментарием, условного оператора никогда не будет выполнено.
Проект CMaNGOS, язык C++
В игре планировалось случайное поведение, но всегда выбирается одна и та же константа SAY_BELNISTRASZ_AGGRO_1.
Проект Vangers: One For The Road, язык C++
Судя по написанному рядом коду, правильный вариант должен быть таким:
Проект RT-Thread, язык C
RT-Thread - это операционная система с открытым исходным кодом в реальном времени для встроенных устройств. Здесь мы видим путаницу между FIFO 0 и FIFO 1. И где-то кто-то столкнётся с глючным устройством.
Ошибка здесь:
Проект Hive, язык Java
Анализатор PVS-Studio указывает сразу на 2 ошибки:
- Строка, хранящаяся в colOrScalar1, не может одновременно равняться строкам "Col" и "Column";
- Строка, хранящаяся в colOrScalar1, не может одновременно равняться строкам "Col" и "Scalar".
Явно присутствует путаница в именах переменных.
Проект Shareaza, язык C++
Правильно:
Примечание
Давайте сделаем небольшую паузу. Есть опасение, что просматривая гору банальных ошибок, мы забудем, зачем мы это делаем.
Задача не в том, чтобы посмеяться над чужим кодом. Всё это не повод тыкать пальцем и приговаривать: "Ха-ха, ну надо же". Этот повод задуматься!
Публикации нашей команды призваны показать, что никто из нас не застрахован от ошибок. Ошибки, описываемые в статье, появляются в коде намного чаще, чем можно ожидать. Ещё важно, что вероятность запутаться в 0, 1, 2 почти не зависит от квалификации программиста.
Полезно осознать, что людям свойственно ошибаться. Без этого нельзя сделать следующий шаг в повышении качества и надёжности кода. Понимая, что все мы ошибаемся, люди начинают стараться выявить ошибки на самых ранних этапах, используя стандарты кодирования, обзоры кода, юнит-тесты, статические и динамические анализаторы. Это очень хорошо.
Зачем написаны и так понятные вещи? К сожалению, общаясь с большим количеством разработчиков, мы вынуждены констатировать, что не всегда это так уж всем понятно. У многих слишком завышена самооценка и они просто не допускают мысли, что способны совершать простые ошибки. Это грустно.
Если вы тимлид/менеджер, то приглашаю заодно ознакомиться вот с этой заметкой.
Проект Qt, язык C++
Правильно:
Проект Android, язык C++
Сразу две опечатки, из-за которых переменные pr2.mStretchMode и pr2.mFallbackMode сравниваются сами с собой.
Проект Boost, язык C++
В самом конце опечатались и поделили переменную p1.z саму на себя.
Проект Clang, язык C++
Да, да, подобные ошибки анализатор PVS-Studio находит и в компиляторах. Правильно:
Проект Clang, язык C++
Правильно:
Проект Qt, язык C++
Проект NCBI Genome Workbench, язык C++
Ошибка в самой первой проверке. Должно быть написано:
Проект NCBI Genome Workbench, язык C++
Была размножена первая строчка условия, но затем программист поспешил и забыл заменить loc1 на loc2.
Проект FlashDevelop, С#
Проект FreeCAD, язык C++
Независимо от условия, выполняется одно и то же действие. Казалось бы, такой простой случай. Как можно было скопировать строчку и не исправить её? Можно.
Проект LibreOffice, язык C++
Классическая Copy-Paste ошибка. Правильно:
Проект LibreOffice, язык C++
И ещё одна классическая Copy-Paste ошибка :). В одном месте 1 на 2 поправили, а в другом забыли.
Проект LibreOffice, язык C++
Здесь не ошибка в замене 1 на 2, а просто не добавили 2 во второе условие.
Возможно, вы немного устали. Поэтому предлагаю заварить чай или кофе, и мы продолжим знакомство с миром чисел 0, 1 и 2.
Проект Geant4 software, язык C++
Надеюсь, вы воспользовались советом и отдохнули. Готовы теперь найти в этом коде ошибку?
Поздравляю читателей, которые заметили ошибку. Вы молодцы!
Кто поленился искать, тех я тоже понимаю. Обзор подобного кода очень утомителен и есть желание как-то побыстрее пойти проверять что-то более интересное. Вот здесь статические анализаторы отлично помогают, так как не устают.
Ошибка в том, что эти две проверки совпадают:
Если изучить код, то становится понятно, что ошибочной является самая первая проверка. Правильно:
Проект CryEngine V, язык C++
Проект TortoiseGit, язык C++
Проект Geant4 software, язык C++
Проект MonoDevelop, С#
Как видите, пока фрагменты кода идут без пояснений. Собственно, тут и пояснять нечего. Можно только повздыхать.
Проект Dolphin Emulator, язык C++
Проект RunAsAdmin Explorer Shim, язык C++
Проект IT++, язык C++
Проект QuantLib, язык C++
Проект Samba, язык C++
Проект Mozilla Firefox, язык C++
Проект Haiku Operation System, язык C++
Проект Qt, язык C++
Ok, теперь давайте чуть-чуть посложнее. Попробуйте ради интереса найти ошибку здесь:
Картинка, чтобы не увидеть ответ сразу, и была возможность подумать.
Правильно, вместо orig->y1 - orig->y1 должно быть написано orig->y1 - orig->y2.
Проект .NET Compiler Platform, язык #
Интересный случай. В целях тестирования требуется запускать потоки в различном порядке. Однако из-за опечатки потоки стартуют всегда одинаково, вследствие чего тест проверяет меньше чем должен.
Правильно:
Проект Samba, язык С
Функция сравнения никогда не вернёт 1, так как условие i2->pid > i2->pid не имеет смысла.
Естественно, это обыкновенная опечатка, и на самом деле должно быть написано:
Проект ChakraCore, язык C++
Последний случай в этой главе. Ура!
Прочие ошибки
Теперь поговорим о менее многочисленных паттернах ошибок, связанных с использованием чисел 0, 1, 2.
Опечатки в условиях, где явно используется константа 0/1/2
Проект ROOT, язык C++
Странно дважды сравнивать переменную fSummaryVrs с 0.
Проект .NET CoreCLR, язык С#
Проект FFmpeg, язык C
Индекс / имя
Ранее мы рассматривали случаи, когда неверен индекс или имя. А вот ситуация, когда сразу и не скажешь, как классифицировать ошибку. Этот пример можно было отнести как к одной, так и к другой главе. Поэтому я решил привести его отдельно.
Проект Mesa 3D Graphics Library, язык C++
Этот код можно поправить так:
И можно поправить так:
Лишний 0
Иногда 0 оказывается лишним и вредным. Из-за него число может превратиться в восьмеричное там, где это не нужно. Или портят форматную строку.
Названные ошибки не подходят для этой статьи, но я считаю, что про них стоит упомянуть. Не буду приводить в статье код с этими ошибками, но если интересно, то на них можно посмотреть здесь:
Забыли написать +1
Проект Haiku Operation System, язык C++
Правильный вариант:
Примечание. Ситуация, когда забыли добавить единицу, вовсе не редка. Я точно помню, что не раз встречал такие случаи. Однако, когда я захотел набрать подобных примеров для статьи, я нашёл только этот пример. Мне жаль, что я не могу напугать вас этими ошибками побольше. Прошу прощения.
Ошибки форматирования (C# )
Чаще всего функции для построения строк оперируют с небольшим количеством аргументов. Вот и получается, что чаще всего ошибки связаны с использованием {0}, {1} или {2}.
Проект Azure PowerShell, язык C#
Опечатались и дважды написали {0}. В результате в строку будет дважды вставлено имя this.Name. А вот имя this.ResourceGroupName не попадёт в создаваемую строку.
Проект Mono, язык C#
Это вообще что-то странное. Нужно вставить то, чего нет. Скорее всего, этот код подвергся неудачному рефакторингу и оказался сломан.
Проект Xenko, язык C#
Программист забыл, что нумерация начинается с {0}, а не с {1}. Правильный код:
Проект .NET Compiler Platform, язык C#
Аргументов явно недостаточно.
Выводы и рекомендации
Мне пришлось продемонстрировать очень много примеров, чтобы показать, что опечатки, связанные с 0, 1 и 2, заслуживают отдельного внимания.
Если бы я просто сказал: "Легко перепутать o1 и o2", вы бы согласились, но не придали этому то значение, какое придадите сейчас, прочитав или хотя бы пролистав статью.
Теперь вы предупреждены, и это хорошо. Предупреждён — значит вооружён. Теперь вы будете более внимательны на обзорах кода и уделите дополнительное внимание переменным, в именах которых увидите 0, 1, 2.
Дать какие-то рекомендации по оформлению кода, чтобы избежать подобных ошибок, сложно. Как вы видели, ошибки встречаются даже в таком простом коде, где и оформлять-то нечего.
Поэтому я не стану призывать избегать 0, 1, 2 и давать переменным длинные имена. Если вместо чисел начать писать First/Second/Left/Right и так далее, то соблазн скопировать имя или выражение будет ещё больше. Возможно, такая рекомендация в конечном итоге вовсе не сократит, а увеличит количество ошибок.
Тем не менее, когда вы пишете много однотипного кода, по-прежнему актуальна рекомендация "табличного оформления кода". Табличное форматирование не гарантирует отсутствие опечаток, но позволяет их легче и быстрее заметить. См. главу N13 в мини-книге "Главный вопрос программирования, рефакторинга и всего такого".
Есть ещё одна хорошая новость. Все рассмотренные в этой статье ошибки найдены с помощью статического анализатора кода PVS-Studio. Соответственно, внедряя в процесс разработки инструменты статического анализа, вы сможете выявить многие опечатки на самом раннем этапе.
Спасибо за внимание. Надеюсь, вам было интересно и страшно. Желаю надёжного кода и поменьше ошибаться с 0, 1, 2, чтобы Фредди не пришёл к вам.
Первоисточник: Блог компании PVS-Studio.