Постепенно и незаметно складывается ситуация, когда сложность серьёзных C++ проектов становится запредельной. К сожалению, теперь C++ программист не может полагаться только на свои силы.
Примечание. Впервые эта статья была опубликована мною в блоге Fluent C++: Why Static Analysis Can Improve a Complex C++ Codebase.
Во-первых, кода стало так много, что уже невозможна ситуация, когда в проекте есть хотя бы парочка программистов, которые знают проект целиком. Например, ядро Linux 1.0.0 содержало около 176 тысяч строк кода. Это много, но была возможность поставить рядом кофе-машину и за пару недель более или менее просмотреть весь код и понять общие принципы его работы. Если же брать ядро Linux 5.0.0, то размер кодовой базы составляет уже около 26 миллионов строк кода. Код ядра вырос почти в 150 раз. Можно только выбрать несколько частей проекта и принимать участие в их развитии. Невозможно сесть и разобраться, как именно это всё работает, какие есть взаимосвязи между различными участками кода.
Во-вторых, язык C++ продолжает быстро развиваться. С одной стороны, это хорошо, так как появляются конструкции, которые позволяют писать более компактный и безопасный код. С другой стороны, в силу обратной совместимости, старые большие проекты становятся разнородными. В них переплетаются старые и новые подходы к написанию кода. Напрашивается аналогия с кольцами на срезе дерева. Из-за этого с каждым годом погружаться в проекты, написанные на C++, становится всё сложнее и сложнее. Нужно одновременно разбираться в коде, как написанном ещё в стиле C с классами, так и в современных подходах (лямбдах, семантике перемещения и т.д.). Чтобы всесторонне изучить С++ требуется слишком много времени. Но поскольку развивать проекты все равно требуется, люди начинают писать код на C++, так и не изучив до конца всех его нюансов. Это приводит к дополнительным дефектам, но нерационально из-за этого остановиться и ждать, когда все разработчики станут идеально знать C++.
Ситуация безнадёжна? Нет. На помощь приходит новый класс инструментов: статические анализаторы кода. Многие бывалые программисты в этот момент скривили губы, как будто им подсунули лимон :). Мол, знаем мы эти ваши линтеры... Сообщений много, толку мало... Да и какой же это новый класс инструментов?! Мы линтеры ещё 20 лет назад запускали.
И всё-таки я рискну утверждать, что это именно новый класс инструментов. То, что было 10-20 лет назад, это совсем не те инструменты, которые сейчас называются статическими анализаторами. Во-первых, я не говорю об инструментах, ориентированных на форматирование кода. Они тоже относятся к инструментам статического анализа, но сейчас мы говорим о выявлении ошибок в коде. Во-вторых, современные инструменты используют сложные технологии анализа, учитывая взаимосвязи между разными функциями и фактически виртуально выполняя некоторые участки кода. Это совсем не те линтеры 20-ти летней давности, построенные на регулярных выражениях. Кстати, нормальный статический анализатор вообще нельзя сделать на регулярных выражениях. Чтобы находить ошибки, используются такие технологии, как анализ потока данных, автоматическое аннотирование методов, символьное выполнение и так далее.
Это всё не абстрактные слова, а реальность, которую я наблюдаю, являясь одним из создателей инструмента PVS-Studio. Загляните в эту статью, чтобы посмотреть благодаря чему анализаторы могут находить интереснейшие ошибки.
Однако намного важнее, что современные статические анализаторы обладают обширными знаниями по паттернам ошибок. Причем анализаторы знают больше, чем даже профессиональные разработчики. Слишком сложно стало учитывать и помнить все нюансы при написании кода. Например, если специально про это где-то не прочитать, то вы никогда не догадаетесь, что вызовы функции memset для затирания приватных данных иногда исчезают, так как с точки зрения компилятора вызов функции memset избыточен. А между тем, это серьезный дефект безопасности CWE-14, который обнаруживается буквально везде. Или кто, например, знает, что опасного в подобном наполнении контейнера?
std::vector<std::unique_ptr<MyType>> v;
v.emplace_back(new MyType(123));
Думаю, далеко не все и не сразу сообразят, что подобный код потенциально опасен и может приводить к утечкам памяти.
Помимо обширного знания паттернов статические анализаторы бесконечно внимательны и никогда не устают. Например, в отличие от человека, им не лень заглянуть в заголовочные файлы, чтобы убедиться, что isspace и sprintf - это настоящие функции, а не безумные макросы, которые всё портят. Подобные случаи демонстрируют суть сложности поиска ошибок в больших проектах: меняется что-то в одном месте, а ломается в другом.
Я убеждён, что в скором времени статический анализ станет неотъемлемой частью DevOps - это будет столь же естественно и необходимо, как использование системы контроля версий. Это уже постепенно происходит и на конференциях, посвященных процессу разработки, где всё чаще упоминается статический анализ как одна из первых линий обороны для борьбы с багами.
Статический анализ выполняет своего рода роль фильтра грубой очистки. Неэффективно искать глупые ошибки и опечатки, используя юнит-тесты или ручное тестирование. Намного быстрее и дешевле исправить их сразу после написания кода, используя для обнаружения проблем статический анализ. Эта мысль, а также важность регулярного применения анализатора, хорошо описана в статье "Внедряйте статический анализ в процесс, а не ищите с его помощью баги".
Кто-то может сказать, что нет смысла в специальных инструментах, так как подобные статические проверки учатся делать и компилятор. Да, это так. Однако, статические анализаторы естественно тоже не стоят на месте и как специализированные инструменты превосходят компиляторы. Например, каждый раз проверяя LLVM мы находим там ошибки с помощью PVS-Studio.
Мир предлагает большое количество инструментов статического анализа кода. Как говорится, выбирайте на свой вкус. Хотите находить множество ошибок и потенциальных уязвимостей ещё на этапе написания кода? Используйте статические анализаторы кода и улучшите качество вашей кодовой базы!
0