Вебинар: ГОСТ Р 71207–2024 — Статический анализ программного обеспечения. Процессы - 13.09
Представим, что у вас идеальный проект. Таски пилятся, компилятор компилирует, статические анализаторы анализируют, релизы релизятся. В какой-то момент вы принимаете волевое решение открыть древний файл, в который никто не залезал уже много лет, и видите, что он в кодировке Windows-1251. При том, что весь проект уже давно перешёл на UTF-8. "Непорядок!" — думаете вы, и лёгким движением руки меняете кодировку. На следующий день на вашем тестовом сервере случается локальный апокалипсис. Думаете, такого не может быть? Тогда предлагаю это обсудить.
История неуспеха
"Так исторически сложилось" — легендарная фраза. Универсальный отбойник для джунов и предвестник беды для опытных программистов. В нашем проекте это причина, по которой значительная часть файлов с исходным кодом находится в кодировке Windows-1251.
В какой-то момент нам надоели проблемы с битыми символами, Doxygen'ом, да и вообще, как бы 2024 год на дворе. Возникла необходимость конвертировать файлы в UTF-8. А чтобы разным утилитам было проще автоматически определять кодировку, решили при конвертации добавлять BOM-заголовок. Создали таску, и прогресс пошёл: таска выполнялась, файлы конвертировались. Казалось бы, антибугурт. Но всё не так просто.
В это же время другой программист выполнял другую таску. Задача была простая: нужно было немного отредактировать код в одном заголовочном файле. Программист решил помочь своим коллегам, немного сократив им фронт работ. Для этого он перевёл затронутый заголовочный файл в кодировку UTF-8 с BOM и закоммитил. Последствия "небольшого" фикса настигли компанию уже на следующий день: сборка и все ночные тесты были в ауте, а тимлид был в бешенстве. Начали разбираться.
При просмотре выхлопа компилятора было обнаружено множество сообщений о нарушении one definition rule (ODR). Складывалось впечатление, что кто-то забыл добавить в заголовочный файл #pragma once. Стали искать по коммитам стартовую точку, с которой начались проблемы, и не нашли. Все правки были достаточно простыми и не добавляли новых заголовочных файлов. Тем не менее спустя некоторое время подозрение пало на коммит с изменением кодировки заголовочного файла. Подозрение превратилось в уверенность, когда мы сделали минимальный воспроизводимый пример.
В этом примере простейший проект всего из пары файлов. Файл functions.h — предкомпилированный заголовочный файл, содержащий объявление класса и функции. Для защиты от двойного включения он содержит #pragma once. Файлы functions.cpp и main.cpp — это юниты трансляции, включающие предкомпилированный заголовочный файл.
Казалось бы, простейший код. Ошибок нет. Но проект не компилируется. Если попробовать собрать проект при помощи GCC (проверял на версиях 12.2.0 и 13.2.0), то вы увидите сообщения о нарушении ODR.
Но стоит вам лишь поменять кодировку файла functions.h на UTF-8 без BOM-заголовка, как все ошибки компиляции пропадут. Вот так GCC показал вам фокус с исчезновением. Жаль только, что исчезла #pragma once из вашего проекта.
Поискав похожие репорты в баг-трекере, я нашёл вот такой. В нём даже есть свежее сообщение с патчем, который, возможно, пофиксит этот баг. Но пока всё глухо. Обновлений не было уже четыре года, а статус тикета по-прежнему unconfirmed. Что примечательно, тикет создан в 2013 году, и с тех пор баг так и не был исправлен.
Что это, если не бугурт?
Внимательные пользователи скажут мне: "Подожди, с 2013 года прошло 11 лет, почему ты тогда писал про 13 лет в заголовке?" Дело в том, что у тикета есть дубликат, и он старше оригинала на два года.
Пользователи GCC могут начать защищать свой компилятор, мол, у нас есть include guards, зачем нам ваша pragma? Развёл тут, понимаешь, дискуссии. Ну, если вы компилируете только под GCC и вам этого достаточно, тогда никаких вопросов к вам нет. Разве что писать макросы и #ifdef'ы менее удобно, чем pragma. Но вот если ваш продукт кроссплатформенный, собирающийся под разными компиляторами, тогда у вас беда и печаль: придётся либо пользоваться исключительно guard'ами, либо писать громоздкие #ifdef'ы специально для GCC.
В копилку проблем добавим third-party библиотеки, в которых может быть #pragma once. Причём с самим заголовочным файлом из third-party библиотеки может быть всё в порядке. Он даже может не иметь BOM-заголовка и находиться в нужной вам кодировке. И даже может не быть предкомпилированным. Ваш проект всё равно свалится, стоит лишь этому хедеру с pragma попасть в другой, который будет предкомпилированным. Например, в stdafx.h.
И хорошо будет, если у вас получится выяснить, из-за какого файла у вас падает сборка. Потому что очень часто в предкомпилированном заголовке находятся хедеры, которые включают хедеры, которые включают другие хедеры и т.д. И проблемный файл может находиться на самом дне этой иерархии. Даже если само наличие pragma не свалит вам сборку, то из-за множества включений хедера в юниты трансляции она может сильно замедлиться.
Для GCC pragma — это "больная тема". Помимо вышеописанного, у вас могут начаться проблемы с шаблонами, и бог знает с чем ещё. Огромное количество багов, связанных с pragma, и отсутствие фиксов для них породили культуру "отмены pragma". Сторонники компилятора GCC часто экстраполируют свой негативный опыт на другие компиляторы и призывают не использовать #pragma once вообще, мол, непредсказуемая вещь. Хотя, казалось бы, удобный инструмент, поддерживаемый во многих компиляторах.
Заключение
Прошло уже как минимум 13 лет с появления бага в компиляторе и четыре года с публикации возможного фикса для него. GCC, не пора ли?
Поддержка пользователей и своевременный фикс их проблем — это очень важно для выживания бизнеса. Мы в PVS-Studio уделяем много времени поддержке и стараемся оперативно выдавать фиксы пользователям, чтобы они могли пользоваться полным функционалом инструмента. Например, вы можете почитать другую мою статью, написанную в соавторстве с коллегой, о том, как мы разбирали 278 гигабайт логов.
Если вы хотите узнать про различные виды багов, и как они могут выглядеть в коде и на практике, приглашаю прочитать статьи моих коллег:
0