PVS-Studio - это инструмент для выявления ошибок в исходном коде программ, написанных на языках С, C++ и C#.
PVS-Studio выполняет статический анализ кода и генерирует отчёт, помогающий программисту находить и устранять ошибки. PVS-Studio выполняет широкий спектр проверок кода, но наиболее силён в поисках опечаток и последствий неудачного Copy-Paste. Показательные примеры таких ошибок: V501, V517, V522, V523, V3001.
Анализатор ориентирован на разработчиков, использующих среду Visual Studio, и может в фоновом режиме выполнять анализ измененных файлов после их компиляции. В идеале ошибки будут обнаружены и исправлены ещё до попадания в репозиторий. Однако ничто не мешает использовать анализатор для проверки всего решения целиком или для встраивания в системы непрерывной интеграции. Эти и иные способы использования анализатора описаны в документации.
PVS-Studio выполняет статический анализ кода и генерирует отчёт, помогающий программисту находить и устранять ошибки. PVS-Studio выполняет широкий спектр проверок кода, но наиболее силён в поисках опечаток и последствий неудачного Copy-Paste. Показательные примеры таких ошибок: V501, V517, V522, V523, V3001.
Анализатор ориентирован на разработчиков, использующих среду Visual Studio, и может в фоновом режиме выполнять анализ измененных файлов после их компиляции. В идеале ошибки будут обнаружены и исправлены ещё до попадания в репозиторий. Однако ничто не мешает использовать анализатор для проверки всего решения целиком или для встраивания в системы непрерывной интеграции. Эти и иные способы использования анализатора описаны в документации.
Главный вопрос программирования, рефакторинга и всего такого. Часть 4
Запись от el_programmer размещена 29.04.2016 в 15:02
Показов 2097
Комментарии 0
Метки c language, c++, coding, cpp, programming, tutorial
|
Часть 1: https://www.cyberforum.ru/blog... g4221.html Часть 2: https://www.cyberforum.ru/blog... g4222.html Часть 3: https://www.cyberforum.ru/blog... g4223.html Полная версия в ПДФ формате: https://yadi.sk/i/LKkWupFjr5WzR Полная версия в ПДФ формате английский вариант: https://yadi.sk/i/zKHIOS84r87nk Содержание 36. Если на вашем компьютере происходят магические события, проверьте память 37. Бойтесь оператора continue внутри do { ... } while(...) 38. С сегодняшнего дня используйте nullptr вместо NULL 39. Почему некорректный код иногда работает 40. Внедрите статический анализ кода 41. Сопротивляйтесь добавлению в проект новых библиотек 42. Не давайте функциям название "empty" Заключение 35. Добавляя в enum новую константу, не забываем поправить операторы switch Фрагмент взят из проекта Appleseed. Код содержит ошибку, которую анализатор PVS-Studio диагностирует следующим образом: V719 The switch statement does not cover all values of the 'InputFormat' enum: InputFormatEntity.
Нередко бывает необходимо добавить новую именованную константу в перечисление (enum). Делать это надо очень аккуратно. Здесь подстерегает распространенный паттерн ошибки: забываем где-то добавить case внутрь switch или поправить цепочку операторов if. Подобную ситуацию можно наблюдать в приведённом коде. В какой-то момент в перечисление InputFormat была добавлена константа InputFormatEntity. Это предположение я делаю на основании того, что эта константа находится в конце. Как правило, программисты добавляют новые константы именно в конец enum. А вот оператор switch исправить забыли. В результате случай, когда "m_format==InputFormatEntity" никак не обрабатывается. Корректный код
Давайте, подумаем, как можно предотвратить такие ошибки, производя рефакторинг кода. Самое простое, но не очень удачное решение, это делать везде "default:", который будет уведомлять об ошибке. Например, так:
1. Ошибку мы можем заметить только на этапе выполнения программы. При чем есть опасность, что ошибка не будет обнаружена на этапе тестирования. Если при выполнении тестов m_format всегда неравна InputFormatEntity, то эта ошибка попадёт в релизную версию продукта. Будет неприятно, когда пользователи начнут сообщать о проблемах. 2. Раз мы считаем, что попасть в default это ошибка, то придётся писать case для всех именованных констант. Это неудобно, особенно если в перечислении этих констант много. Иногда действительно удобно обрабатывать многие ситуации одинаково в ветке default. Я предлагаю решать эту проблему организационным образом. Он тоже не идеален, но хоть что-то. Когда в коде вы проверяете значения переменной типа enum, оставляйте комментарий специального вида. Можно использовать какое-то ключевое слово и имя перечисления. Пример:
Обговорите это со всеми коллегами, так как каждый должен начать использовать этот комментарий. Если кто-то будет забывать их писать, то всё развалится. Внесите правило по написанию комментариев в стандарт кодирования. 36. Если на вашем компьютере происходят магические события, проверьте память Я думаю вы устали от бесконечного разбора паттернов программистских ошибок. Немного отдохнём и отвлечемся Итак, типовая ситуация - ваша программа работает неправильно. Но вы не можете дать объяснение происходящему. В таких ситуациях я всегда призываю не спешить перекладывать вину на кого-то ещё, а сосредоточиться на коде вашей программы. В 99.99% случаев причиной неправильно работы программы, является именно ошибка, которую допустил кто-то из разработчиков вашей команды. Причем, часто эта ошибка весьма глупа и банальна. Вот и ищите её! То, что ошибка, проявляется нерегулярно, ничего не означает. Просто у вас завёлся Heisenbug. И упаси вас боже, обвинять компилятор, что он неправильно собирает вашу программу. Такое конечно бывает, но крайне редко. Сами же потом будете глупо выглядеть, когда выяснится, что вы просто не умеете правильно обращаться, скажем, с оператором sizeof(). У меня есть интересная заметка в блоге на эту тему: Во всём виноват компилятор. Но чтобы восстановить истину, я должен сказать, что бывают и исключения. Очень-очень редко виноват не ваш код. Надо знать о существовании такой возможности. Это позволит не поседеть раньше времени. Продемонстрирую это на примере, который произошел однажды со мной. Благо, у меня остались соответствующие скриншоты. У меня отказывался правильно вести себя простой тестовый проект, который я готовил для демонстрации работы анализатора Viva64 (это предшественник PVS-Studio). После долгих разбирательств выяснилось, что сбоит одна из ячеек памяти. Вернее, один единственный бит. На картинке показано, что я, находясь в режиме отладки, записываю в злосчастную ячейку памяти значение 3: После изменения памяти, отладчик считывает значения для показа в окне. И показывает число 2: Видите, там 0x02. Хотя я вводил значение 3. Младший бит всегда равен нулю. Программа тестирования памяти подтвердила наличие проблемы. Забавно, что компьютер работал стабильно, и никаких проблем с ним не возникало. Замена планки памяти по гарантии позволила наконец моей программе работать правильно. Мне очень повезло - я имел дело с простой тестовой программой. И все равно я потратил немало времени, чтобы понять, что происходит. До этого я пару часов изучал ассемблерный листинг, пытаясь найти причину странного поведения. Да, да, я винил в тот момент компилятор. Не представляю, сколько бы я потратил сил и душевного здоровья, если бы это была какая-то настоящая программа. Спасибо провидению, что мне пришлось в тот момент отлаживать именно демонстрационную утилиту! Рекомендация Всегда ищите ошибку в своём коде. Не старайтесь переложить ответственность. Однако, если ошибка вот уже неделю повторяется только на вашем компьютере, это повод заподозрить неладное. Продолжайте искать ошибку, но когда будете уходить домой, запустите на ночь тест оперативной памяти. Возможно, это простое действие спасёт тысячи ваших нервных клеток от перегорания. 37. Бойтесь оператора continue внутри do { ... } while(...) Фрагмент взят из проекта Haiku (преемница операционной системы BeOS). Код содержит ошибку, которую анализатор PVS-Studio диагностирует следующим образом: V696 The 'continue' operator will terminate 'do { ... } while (FALSE)' loop because the condition is always false.
Оператор continue ведет себя внутри конструкции do { } while () не так, как ожидают некоторые программисты. Когда вызывается оператор continue, в любом случае выполняется проверка условия окончания цикла. Попробую пояснить эти тему подробнее. Предположим, что программист пишет кода вида:
Когда же программист пишет код:
Приведёт непонимание как работает continue к ошибке или нет, зависит от везения. Однако, ошибка точно произойдёт, если условие цикла всегда ложно, как в коде, показанном в самом начале. Программист планировал выполнять определённые действия, начиная новые итерации с помощью оператора continue. Об этом намерении свидетельствует присутствующий в коде комментарий "//try again". Однако, никакого "again" не будет, так как используется условие (false), после вызова continue цикл будет остановлен. Фактически получается, что в конструкции do { ... } while (false); оператор continue эквивалентен по своему действию оператору break. Корректный код Вариантов написать корректный код много. Один из них - создать вечный цикл. Для его возобновления использовать continue, а для остановки - оператор break.
Всеми способами старайтесь избегать оператор continue внутри do { ... } while (...);. Уж очень эта конструкция обманчива. Даже если вы отлично знаете, как всё работает, всё равно не используйте этот оператор. Дело в том, что ошибиться можете не только вы, но и ваши коллеги - они могут неправильно прочитать код и в результате неправильно его модифицировать. Никогда не устану упоминать: хороший программист, этот не тот, кто знает и умеет использовать хитрые конструкции языка, а тот, кто пишет простой и понятный код, который легко сопровождать даже новичку. 38. С сегодняшнего дня используйте nullptr вместо NULL В новых стандартах языка С++ появилось много нужного и полезного. Есть и то, что я бы не спешил использовать, по крайней мере рьяно. Но есть и такие нововведения, которые нужно взять на вооружение немедленно. Они однозначно сразу приносят пользу. Одним из таких нововведений является ключевое слово nullptr, которое призвано заменить макрос NULL. Напомню, NULL в С++ это ни что иное, как просто 0. Может показаться, что это синтаксический сахар и не более того. Ну какая разница, пишем мы nullptr или NULL? А разница есть! Использование nullptr реально позволяет избежать разнообразных ошибок. Я продемонстрирую это на примерах. Представим, что имеется 2 перегруженных функции:
Если бы программист использовал nullptr, такой бы ошибки не возникло. В этом случае будет выбрана именно первая функция. Ещё можно написать вот такой код:
Важно то, что программист решил в случае неизвестной ошибки сгенерировать исключение и "отправить" во внешний мир нулевой указатель. На самом деле это не указатель, а int. В результате обработка исключения пойдет не так, как ожидал программист. Код "throw nullptr;" спасает нас от недоразумения, но это вовсе не значит, что я считаю подобный код нормальным и хорошим. В ряде случаев, если использовать nullptr, некорректный код просто не будет компилироваться. Предположим, что какая-то WinApi функция возвращает тип HRESULT. Тип HRESULT не имеет ничего общего с указателем. Однако, вполне можно написать бессмысленный код вида:
Я думаю, что вы поняли идею. Таких примеров можно приводить много, но всё это синтетические примеры, а это всегда не очень убедительно. Есть ли какие-то реальные примеры? Да, есть. Вот один из них, только он не такой красивый, короткий и простой. Код взят из проекта MTASA. В природе существует RtlFillMemory(). Это может быть настоящая функция или просто макрос, но это не важно. Это аналог функции memset(), но местами поменян 2 и 3 аргумент. Вот как может быть объявлен этот макрос:
.А вот собственно и код, использующий макрос FillMemory.
Код скомпилировался из-за того, что NULL это 0; в результате заполняется 0 элементов массива. На самом деле, ошибка не только в этом - NULL здесь вообще не уместен. Функция memset() работает с байтами, поэтому нет смысла просить её заполнить память значениями NULL, это какая-то абракадабра. Правильный код должен быть таким:
Примечание. Я понимаю, что в данном случае NULL не виноват, тем не менее, именно из-за NULL получилось написать неправильный код, который компилируется без предупреждений. Рекомендация Начните использовать nullptr и внесите соответствующий пункт в стандарт кодирования вашей компании. Прямо сейчас. Использование nullptr позволит избегать некоторых глупых ошибок и тем самым немного ускорит процесс разработки приложения. 39. Почему некорректный код иногда работает Фрагмент взят из проекта Miranda NG. Код содержит ошибку, которую анализатор PVS-Studio диагностирует следующим образом: V502 Perhaps the '?:' operator works in a different way than it was expected. The '?:' operator has a lower priority than the '|' operator..
Выше мы рассмотрели множество ситуаций, которые приводят к неправильной работе программ, но я хочу затронуть вот какую интересную тему. Так бывает, что совершенно неправильный код иногда правильно работает. Опытных программистов этим не удивить, но новичкам, изучающим C/C++, думаю будет интересно рассмотреть один из таких примеров. Впрочем, опытным программистам будет тоже не лишним раз напомнить, чтобы они не жадничали расставлять скобки в сложных выражениях. Итак, надо вызвать функцию CheckMenuItem() с определённым набором флагов. Если значение переменной bShowAvatar истинно, то нужен флаг MF_BYCOMMAND и MF_CHECKED. Иначе: MF_BYCOMMAND и MF_UNCHECKED. Для этого написано выражение с использование злосчастного тернарного оператора: MF_BYCOMMAND | dat->bShowAvatar ? MF_CHECKED : MF_UNCHECKED Дело в том, что приоритет оператора | выше приоритета оператора ?: (см. Приоритет операций в языке Си/Си++). В результате имеют место сразу 2 ошибки. Первая ошибка: изменилось условие. Условием является не переменная "dat->bShowAvatar", а выражение "MF_BYCOMMAND | dat->bShowAvatar". Вторая ошибка: выбирается только один из двух флагов: MF_CHECKED или MF_UNCHECKED. Флаг MF_BYCOMMAND "потерялся". При всём при этом, код работает совершенно правильно! Причина - счастливое стечение обстоятельств и везение программиста. Ему повезло в том, что флаг MF_BYCOMMAND равен 0x00000000L. Так как флаг MF_BYCOMMAND равен 0, он не оказывает никакого воздействия. Опытные программисты уже всё поняли, но для новичков разберу этот момент поподробнее. Посмотрим в начале на правильное выражение с дополнительными круглыми скобками: MF_BYCOMMAND | (dat->bShowAvatar ? MF_CHECKED : MF_UNCHECKED) Подставим вместо макросов числовые значения: 0x00000000L | (dat->bShowAvatar ? 0x00000008L : 0x00000000L) Если один из операндов оператора | является 0, то выражение можно упростить: dat->bShowAvatar ? 0x00000008L : 0x00000000L Теперь рассмотрим некорректный вариант кода: MF_BYCOMMAND | dat->bShowAvatar ? MF_CHECKED : MF_UNCHECKED Подставим вместо макросов числовые значения: 0x00000000L | dat->bShowAvatar ? 0x00000008L : 0x00000000L В подвыражении "0x00000000L | dat->bShowAvatar" один из операндов оператора | является 0. Сократим выражение: dat->bShowAvatar ? 0x00000008L : 0x00000000L Как видите, в результате получили одно и то же выражение. Именно поэтому код с ошибкой работает правильно. Вот такие чудеса нам иногда дарит программирование! Корректный код Код можно поправить по-разному - можно добавить круглые скобки; можно добавить промежуточную переменную, возможно, неплохим вариантом будет использовать старый добрый оператор if:
Рекомендация Рекомендация проста - старайтесь избегать сложных выражений, особенно если в них вам потребовался тернарный оператор и не жалейте круглые скобки. Как уже говорилось ранее в главе N4, оператор ?: очень опасен. Легко забыть, что он имеет очень низкий приоритет и легко составить некорректное выражение. Как правило, ?: используют там, где хотят уместить побольше операторов в одну строчку кода, не делайте так. 40. Внедрите статический анализ кода Странно прочитать столько текста, написанного разработчиком статического анализатора кода, и не услышать рекомендации о его использовании. Исправляюсь, вот она. Фрагмент взят из проекта Haiku (преемница операционной системы BeOS). Код содержит ошибку, которую анализатор PVS-Studio диагностирует следующим образом: V501 There are identical sub-expressions to the left and to the right of the '<' operator: lJack->m_jackType < lJack->m_jackType
Простая опечатка: в правой части вместо rJack случайно вновь написали lJack. Опечатка то простая, а ситуация сложная. Дело в том, что здесь стиль программирования или другие приемы бессильны. Люди ошибаются, набирая текст программ, и ничего с этим не сделаешь. Важно подчеркнуть, что это не проблема каких-то конкретных людей или проектов. Всем людям свойственно ошибаться, и это делают даже профессионалы в серьезных проектах. Итак, проблема существует, и относится она вовсе не к лабораторным работам студентам. Корректный код
В начале о подходах, которые бессильны:
Что может помочь:
Сразу скажу, что у каждой методологии есть свои сильные и слабые стороны, поэтому наиболее качественный и надёжный код можно получить только их сочетанием. Обзоры кода (code review) позволяют выявить множество разнообразнейших ошибок, а заодно улучшить код в плане читаемости. К сожалению, тщательный совместный обзор кода весьма дорог и утомителен, и при том всё равно не даёт гарантию. Очень сложно сохранить внимание и найти опечатку, рассматривая выражения вида:
Статический анализаторы кода - это просто программы, а не искусственный интеллект. Анализатор не замечает многие ошибки и наоборот, часто ругается на корректный код; но при всех этих недостатках это крайне полезный инструмент. Он может выявить множество ошибок на самом раннем этапе. Статический анализ кода можно рассматривать как более дешёвую альтернативу Code Review. Программа вместо человека быстро изучает код и предлагает более внимательно проверить определённые фрагменты кода. Естественно, я предлагаю начать использовать разрабатываемый нами анализатор PVS-Studio. Хотя конечно, свет клином на нём не сошелся, и есть множество других платных и бесплатных инструментов. Например, можно начать знакомство с методологией статического анализа с бесплатного открытого анализатора Cppcheck. Множество инструментов перечислено на странице Wikipedia: List of tools for static code analysis. Важное:
Попробуйте статические анализаторы кода, вам они понравятся, это очень хорошее гигиеническое средство. Напоследок рекомендую ещё вот эту статью от Джона Кармака: Статический анализ кода. 41. Сопротивляйтесь добавлению в проект новых библиотек Итак, вам понадобилось реализовать в проекте функциональность X. Теоретики разработки программного обеспечения в этот момент говорят, что для этого нужно взять уже существующую библиотеку Y и использовать её для реализации необходимых вам вещей. Собственно, это классический подход в разработке программного обеспечения - повторное использование своих или чужих наработок (сторонних библиотек). Именно этим путём движется большинство программистов. Однако, теоретики в статьях и книгах, забывают упомянуть, в какой ад превращается поддержка несколько десятков сторонних библиотек, живущих в вашем проекте, скажем, по прошествии 10 лет. Я рекомендую всячески сопротивляться добавлению в проект каждой новой библиотеки. Прошу понять меня правильно - я вовсе не говорю, что не надо использовать библиотеки и писать всё самостоятельно. Это просто-напросто глупо. Дело в том, что часто новая библиотека добавляется в проект по прихоти одного разработчика с целью использовать в ней какую-то маленькую "фитюльку". Добавить новую библиотеку несложно, вот только потом всей команде много лет придётся нести груз её поддержки. Наблюдая за развитием некоторых больших проектов, я могу перечислить ряд проблем из-за наличия большого количества сторонних библиотек. Наверное, я перечислю далеко не все проблемы, но даже следующий список должен побудить вас задуматься:
Ещё раз подчеркну: я не призываю вас отказаться от использования сторонних библиотек. Если в программе вам понадобилось работать с изображениями в формате PNG, то вам надо взять библиотеку LibPNG и не изобретать велосипед. Но даже работая с PNG, надо остановиться и подумать. А нужна ли библиотека? Какие операции нужно выполнять с изображениями? Быть может, если вся задача сводится к тому, чтобы сохранить какое-то изображение в *.png - файл, можно обойтись системными функциями. Например, если у вас Windows приложение, то вам поможет WIC. А если вы уже используете библиотеку MFC, то вообще не надо усложнять код, ведь есть класс CImagе (см. обсуждение на сайте StackOverflow). Минус одна библиотека - отлично! Приведу пример из собственной практики. В процессе разработки анализатора PVS-Studio, в паре диагностик потребовалось применять простые регулярные выражения. Вообще, я убеждён, что регулярным выражениям не место в статическом анализе - это крайне неэффективный подход. Я даже писал статью на эту тему. Однако иногда, в какой-то строке нужно бывает что-то найти с помощью регулярного выражения. Можно было-бы "прикрутить" какую-то из существующих библиотек. Было понятно, что все они будут избыточны, но ведь регулярные выражения все равно нужны и нужно было принять какое-то решение. Совершенно случайно, именно в тот момент я читал книгу "Beautiful Code" (ISBN 9780596510046). Эта книга о простых и изящных решениях; в ней я повстречал крайне простую реализацию регулярных выражений. Буквально несколько десятков строк. И всё! Я взял из книги эту реализацию и начал использовать в PVS-Studio. И знаете, что? До сих пор возможностей этой реализации нам хватает. Какие-то сложные регулярные выражения нам просто не нужны. Итог. Вместо того, чтобы в проекте появилась какая-то дополнительная библиотека, было потрачено около получаса времени на написание нужной функциональности. Было подавлено желание использовать библиотеку "на все случаи жизни". Как оказалось, это было правильное решение. Это подтверждается, тем что в течении нескольких лет эта самая функциональность "на все случаи" не понадобилась. Этот случай окончательно убедил меня, что надо по возможности искать простые решения. По возможности, отказываясь от библиотек, вы делаете проект более простым. Возможно, читателям будет интересно узнать, что же это за такой код для поиска по регулярным выражениям. Перепечатаю его из книги - посмотрите, как элегантно. Это код был мной немного изменён при интеграции в PVS-Studio, но его суть не изменилась. Итак, код из книги:
Рекомендация Сопротивляйтесь добавлению в проект новых библиотек. Добавлять следует только когда, когда очевидно, что без библиотеки не обойтись. Вот некоторые возможные манёвры:
P.S. Многим рассказанное здесь придется не по душе. Например, то, что я рекомендую использовать не переносимую универсальную библиотеку, а допустим WinAPI. На это будут возражения, основанные на том, что тем самым мы привязываем проект к одной операционной системе. И потом будет очень сложно сделать программу переносимой. Я с этим не согласен. Часто идея "потом перенесем на другу операционную систему" живет только в голове разработчика. На самом деле такая задача вообще может быть никогда не поставлена руководством. Или проект "загнётся" из-за излишней сложности и универсальности, ещё до момента популярности и необходимости портирования. Плюс не забывайте пункт (8) в списке проблем, приведенный выше. 42. Не давайте функциям название "empty" Фрагмент взят из проекта WinMerge. Код содержит ошибку, которую анализатор PVS-Studio диагностирует следующим образом: V530 The return value of function 'empty' is required to be utilized.
Программист хотел очистить строки strLeft и strRight. Строки имеют тип String, который представляет собой не что иное, как std::wstring. Для очистки он вызвал функцию empty(). Это неправильно - функция empty() не изменяет объект, а только возвращает информацию о том, является строка пустой или нет. Корректный код Чтобы исправить ошибку, следует заменить функцию empty() на clear() или erase(). Разработчики WinMerge предпочли erase() и сейчас код выглядит так:
Причина подобной ошибки в неудачном имени "empty()". Дело в том, что в разных библиотеках эта функция может обозначать два разных действия. В одних библиотеках функция empty() очищает объект; в других - возвращает информацию о том, является ли объект пустым. Слово "empty" плохое. Каждый понимает его по-своему: кто-то считает его "действием", кто-то считает его "запросом информации". Отсюда вся эта путаница и ошибки. Выход только один - не делайте в своих классах функцию с именем "empty".
Это весьма распространенный паттерн ошибки. Конечно, менять такие классы как std::string уже поздно, но давайте хотя бы дальше не умножать зло! Заключение Надеюсь вам понравился этот сборник советов. Конечно, предупредить о всех способах написать программу неправильно невозможно, да в этом и нет смысла. Моей целью было предостеречь программиста и развить в нем чувство опасности. Возможно, когда программист в очередной раз столкнется с чем-то непонятным, он вспомнит о моих наставлениях и не станет торопиться. Иногда несколько минут изучения документации или написание более простого/ясного кода позволит избежать внесения скрытой ошибки, которая затем несколько лет отравляла бы жизнь пользователям и коллегам. Пользуясь случаем приглашаю всех желающих последовать за мной в Twitter: @Code_Analysis. Желаю всем безбажных программ. С уважением, Андрей Карпов. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Метки c language, c++, coding, cpp, programming, tutorial
Размещено в Без категории
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 0
Комментарии

.

