﻿# Насекомое 13 лет сидит в вашем компиляторе и не собирается оттуда вылезать

Представим, что у вас идеальный проект\. Таски пилятся, компилятор компилирует, статические анализаторы анализируют, релизы релизятся\. В какой\-то момент вы принимаете волевое решение открыть древний файл, в который никто не залезал уже много лет, и видите, что он в кодировке Windows\-1251\. При том, что весь проект уже давно перешёл на UTF\-8\. "Непорядок\!" — думаете вы, и лёгким движением руки меняете кодировку\. На следующий день на вашем тестовом сервере случается локальный апокалипсис\. Думаете, такого не может быть? Тогда предлагаю это обсудить\.

![1141_Insect_sits_in_the_compiler_ru/image1.png](https://import.viva64.com/docx/blog/1141_Insect_sits_in_the_compiler_ru/image1.png)

**История неуспеха**

"Так исторически сложилось" — легендарная фраза\. Универсальный отбойник для джунов и предвестник беды для опытных программистов\. В нашем проекте это причина, по которой значительная часть файлов с исходным кодом находится в кодировке Windows\-1251\.

В какой\-то момент нам надоели проблемы с битыми символами, Doxygen'ом, да и вообще, как бы 2024 год на дворе\. Возникла необходимость конвертировать файлы в UTF\-8\. А чтобы разным утилитам было проще автоматически определять кодировку, решили при конвертации добавлять BOM\-заголовок\. Создали таску, и прогресс пошёл: таска выполнялась, файлы конвертировались\. Казалось бы, антибугурт\. Но всё не так просто\.

В это же время другой программист выполнял другую таску\. Задача была простая: нужно было немного отредактировать код в одном заголовочном файле\. Программист решил помочь своим коллегам, немного сократив им фронт работ\. Для этого он перевёл затронутый заголовочный файл в кодировку UTF\-8 с BOM и закоммитил\. Последствия "небольшого" фикса настигли компанию уже на следующий день: сборка и все ночные тесты были в ауте, а тимлид был в бешенстве\. Начали разбираться\.

При просмотре выхлопа компилятора было обнаружено множество сообщений о нарушении [one definition rule \(ODR\)](https://en.cppreference.com/w/cpp/language/definition#One_Definition_Rule)\. Складывалось впечатление, что кто\-то забыл добавить в заголовочный файл _\#pragma once_\. Стали искать по коммитам стартовую точку, с которой начались проблемы, и не нашли\. Все правки были достаточно простыми и не добавляли новых заголовочных файлов\. Тем не менее спустя некоторое время подозрение пало на коммит с изменением кодировки заголовочного файла\. Подозрение превратилось в уверенность, когда мы сделали [минимальный воспроизводимый пример](https://github.com/ComeInRage/gcc_pragma_once_bug)\.

В этом примере простейший проект всего из пары файлов\. Файл _functions\.h_ — [предкомпилированный заголовочный файл](https://pvs-studio.ru/ru/blog/posts/cpp/0265/), содержащий объявление класса и функции\. Для защиты от двойного включения он содержит _\#pragma once_\. Файлы _functions\.cpp_ и _main\.cpp_ — это юниты трансляции, включающие предкомпилированный заголовочный файл\.

Казалось бы, простейший код\. Ошибок нет\. Но проект не компилируется\. Если попробовать собрать проект при помощи GCC \(проверял на версиях 12\.2\.0 и 13\.2\.0\), то вы увидите сообщения о нарушении ODR\.

![1141_Insect_sits_in_the_compiler_ru/image2.png](https://import.viva64.com/docx/blog/1141_Insect_sits_in_the_compiler_ru/image2.png)

Но стоит вам лишь поменять кодировку файла _functions\.h_ на UTF\-8 без BOM\-заголовка, как все ошибки компиляции пропадут\. Вот так GCC показал вам фокус с исчезновением\. Жаль только, что исчезла _\#pragma once_ из вашего проекта\.

Поискав похожие репорты в баг\-трекере, я нашёл вот [такой](https://gcc.gnu.org/bugzilla/show_bug.cgi?id=56549)\. В нём даже есть свежее сообщение с патчем, который, возможно, пофиксит этот баг\. Но пока всё глухо\. Обновлений не было уже четыре года, а статус тикета по\-прежнему unconfirmed\. Что примечательно, тикет создан в 2013 году, и с тех пор баг так и не был исправлен\.

Что это, если не бугурт?

![1141_Insect_sits_in_the_compiler_ru/image3.gif](https://import.viva64.com/docx/blog/1141_Insect_sits_in_the_compiler_ru/image3.gif)

Внимательные пользователи скажут мне: "Подожди, с 2013 года прошло 11 лет, почему ты тогда писал про 13 лет в заголовке?" Дело в том, что у тикета есть [дубликат](https://gcc.gnu.org/bugzilla/show_bug.cgi?id=49837), и он старше оригинала на два года\.

Пользователи GCC могут начать защищать свой компилятор, мол, у нас есть [include guards](https://en.wikipedia.org/wiki/Include_guard), зачем нам ваша _pragma_? Развёл тут, понимаешь, дискуссии\. Ну, если вы компилируете только под GCC и вам этого достаточно, тогда никаких вопросов к вам нет\. Разве что писать макросы и _\#ifdef_'ы менее удобно, чем _pragma_\. Но вот если ваш продукт кроссплатформенный, собирающийся под разными компиляторами, тогда у вас беда и печаль: придётся либо пользоваться исключительно guard'ами, либо писать громоздкие _\#ifdef_'ы специально для GCC\.

В копилку проблем добавим third\-party библиотеки, в которых может быть _\#pragma once_\. Причём с самим заголовочным файлом из third\-party библиотеки может быть всё в порядке\. Он даже может не иметь BOM\-заголовка и находиться в нужной вам кодировке\. И даже может не быть предкомпилированным\. Ваш проект всё равно свалится, стоит лишь этому хедеру с _pragma_ попасть в другой, который будет предкомпилированным\. Например, в _stdafx\.h_\.

И хорошо будет, если у вас получится выяснить, из\-за какого файла у вас падает сборка\. Потому что очень часто в предкомпилированном заголовке находятся хедеры, которые включают хедеры, которые включают другие хедеры и т\.д\. И проблемный файл может находиться на самом дне этой иерархии\. Даже если само наличие _pragma_ не свалит вам сборку, то из\-за множества включений хедера в юниты трансляции она может сильно [замедлиться](https://gcc.gnu.org/bugzilla/show_bug.cgi?id=58770)\.

Для GCC _pragma_ — это "больная тема"\. Помимо вышеописанного, у вас могут начаться проблемы с [шаблонами](https://gcc.gnu.org/bugzilla/show_bug.cgi?id=64117#c0), и бог знает с чем ещё\. Огромное количество багов, связанных с _pragma_, и отсутствие фиксов для них породили культуру "отмены _pragma_"\. Сторонники компилятора GCC часто экстраполируют свой негативный опыт на другие компиляторы и призывают не использовать _\#pragma once_ вообще, мол, непредсказуемая вещь\. Хотя, казалось бы, удобный инструмент, поддерживаемый во многих компиляторах\.

**Заключение**

Прошло уже как минимум 13 лет с появления бага в компиляторе и четыре года с публикации возможного фикса для него\. GCC, не [пора](https://pvs-studio.ru/ru/blog/posts/cpp/0900/) ли?

Поддержка пользователей и своевременный фикс их проблем — это очень важно для выживания бизнеса\. Мы в PVS\-Studio уделяем много времени поддержке и стараемся оперативно выдавать фиксы пользователям, чтобы они могли пользоваться полным функционалом инструмента\. Например, вы можете почитать другую мою статью, написанную в соавторстве с коллегой, о том, как мы [разбирали 278 гигабайт логов](https://pvs-studio.ru/ru/blog/posts/cpp/1005/)\.

Если вы хотите узнать про различные виды багов, и как они могут выглядеть в коде и на практике, приглашаю прочитать статьи моих коллег:

1. [Улыбка сквозь баги](https://pvs-studio.ru/ru/blog/posts/cpp/1131/)
1. [Не исправил, а проработал принятие: как некоторые баги в играх стали фичами](https://pvs-studio.ru/ru/blog/posts/1108/)
1. [Баги, которые наделали немало шума](https://pvs-studio.ru/ru/blog/posts/1114/)
1. [30 лет DOOM: новый код — новые баги](https://pvs-studio.ru/ru/blog/posts/cpp/1087/)
1. [Проверка игрового движка qdEngine, часть первая: топ 10 предупреждений PVS\-Studio](https://pvs-studio.ru/ru/blog/posts/cpp/1119/)
1. [От винта\! Смотрим движок War Thunder и говорим с его создателями](https://pvs-studio.ru/ru/blog/posts/cpp/1098/)