Хедеры (header files, headers) существуют, чтобы определить интерфейс доступа к функциональности модулей и даже целых библиотек путём описания прототипов функций, новых типов данных, глобальных переменных и дефиниций.
Но давайте представим, что имеется хедер какой-либо библиотеки, в котором объявлена функция с неиспользуемым аргументом, а ваш проект компилируется с помощью GCC/Clang с флагами -Wall -Wextra -Werror
и всё падает на тех модулях, которые включают этот злополучный хедер. В итоге из-за сраного хедера вы не можете использовать -Werror
вообще, а если вас раздражают предупреждения, то и от -Wextra
придётся отказаться.
Ещё одна проблема, вызываемая компилируемым кодом в хедерах - это то, что его придётся компилировать. Особенно забавно выглядит костыль, решающий эту проблему, под названием предварительная компиляция хедеров (precompiled headers). Более того, этот костыль очень трудно поддерживается системами сборки. Хотя в виде контраргумента можно назвать меня неосилятором, но факт увеличения времени компиляции из-за пустых конструкторов и деструкторов, небольших методов и инлайнов остаётся фактом.
Ситуации с компилируемым кодом в хедерах больше характерны для C++, нежели для C, где короткие методы классов реализуются прям в прототипе класса и содержат код, генерирующий предупреждения. Лично я сталкивался с подобным при работе с симулятором Menge. Да что там спецсофт, даже стандартная библиотека сожержит компилируемый код в хедарах! Это я ещё, оказывается, про шаблоны (templates) не слышал.
Это утверждение уже скорее моё мнение, чем правило хорошего кода. Такой подход практикуется в компиляторе диалекта ANSI C для операционной системы Plan9 - более подробно в статье Роба Пайка (или на web.archive.org если оригинальная ссылка как обычно недоступна). Так же в другой статье про язык го Роб Пайк рассказывает чего можно ожидать от вложенных хедеров с защитой от повторного включения при масштабировании проекта. Хотя в целом однозначную оценку тому или иному подходу дать трудно - исследований на эту тему я не нашёл, но постараюсь объяснить в чём отличия этих двух подходов.
Если вы используете подход с хедерами, включающими зависимые хедеры и обрамлёнными в обязательном порядке защитой от повторного включения, то наверняка у вас есть несколько хедеров, зависящих от какого-то одного более простого и есть ситуации, когда вы используете два или более таких хедеров в одном модуле (модуль, он же translation unit, он же, как правило, .c/.cpp
файл, который компилируется в объектник). Таким образом, включая два и более таких хедеров, компилятор будет пару раз или более заглядывать в тот маленький общий хедер, затрачивая на это какие-то ресурсы. Какие это по объёму ресурсы - конкретно сказать невозможно, нужно проводить экспериментальную оценку для различных компиляторов.
Есть и преимущество: если хедер a.h
был изменён и больше не зависит от другого более общего хедера b.h
, то b.h
можно спокойно исключить из a.h
, в отличие от другого подхода.
Другой подход (из Plan9) как раз заключается в принципиальном отказе от включения хедеров в хедеры, тем самым обеспечивая только одну попытку включения компилятором каждого упомянутого хедера в рамках компиляции одного модуля. Если a.h
зависит от b.h
, то b.h
должен быть включён где-нибудь перед a.h
в модуле. Но в процессе разработки a.h
может перестать зависеть от b.h
и тогда придётся прочёсывать все модули, включающие a.h
и исключать из зависимости модуля файл b.h
, предварительно удостоверившись, что от него больше ничего не зависит.
Но для таких ситуаций как раз существует утилита под незамысловатым называнием include-what-you-use, позволяющая отследить и исключить неиспользуемые хедеры. Для обоих подходов она будет полезна.
Ну, а у плюсовщиков всё ещё есть прекомпилированные хедеры, в которые можно собрать все хедеры всех зависимостей. Тогда оригинальные хедеры компилятор даже читать не будет и сразу возьмётся за прекомпилированные. Их можно во многих случаях хотя бы рассматривать как своего рода панацею против бед, связанных с хедерами.
Только ситхи всё возводят в абсолют. Иногда лучше использовать компилируемый код прямо в хедерах, например инлайны, но очень осторожно. Включайте хедеры в хедеры если вам кажется это невероятно эффективным и наоборот. Знайте больше, думайте своей головой и трезво оценивайте свои возможности и потребности. Как сказал Анатолий Вассерман (а до него наверняка ещё кто-нибудь), инженерная деятельность - это поиск компромиссов; если удаётся достичь чего-то без компромиссов, то это уже изобретательский прорыв.
Спустя почти год с новым опытом мой взгляд на описываемые в статье вещи изменился. Укажу на все недочёты и ошибки здесь, потому что статью править не хочу, ибо её нужно вообще переписать или просто уничтожить, но пусть лучше будет.
Ох не стоит помещать глобальные переменные в хедер, ибо будет ошибка линковки типа
multiple definition
, если хедер включён в больше, чем один модуль (translation unit). Но можно помещать extern прототипы таких переменных.Если только не понадобилось вдруг поместить туда функцию с классом хранения
static inline
.Вот про C++ мне вообще не стоило ничего говорить. Это отдельный мир с темплейтами, с которым я слабо знаком. Вообще всё сказанное мною про C++ относительно хедеров может оказаться чушью.
Это актуально только для хедеров внутри проекта. Хедеры библиотек построенные таким образом могут вызвать фрустрацию у пользователей.