Хороший стиль заголовка C - PullRequest
9 голосов
/ 24 марта 2011

Мои заголовки C обычно напоминают следующий стиль, чтобы избежать многократного включения:

#ifndef <FILENAME>_H
#define <FILENAME>_H

// define public data structures / prototypes, macros etc.

#endif  /* !<FILENAME>_H */

Однако в своих Замечаниях по программированию в C Роб Пайк приводит следующий аргумент в отношении заголовочных файлов:

Есть небольшой танец с участием #ifdef, который может помешать чтению файла дважды, но на практике это обычно делается неправильно - #ifdef находятся в самом файле, а не в файле, который его включает. Результатом часто являются тысячи ненужных строк кода, проходящих через лексический анализатор, который (в хороших компиляторах) является самой дорогой фазой.

С одной стороны, Пайк - единственный программист, которым я действительно восхищаюсь. С другой стороны, если поместить несколько #ifdef в несколько исходных файлов вместо одного #ifdef в одном заголовочном файле, то это излишне неловко.

Как лучше всего решить проблему множественного включения?

Ответы [ 7 ]

11 голосов
/ 24 марта 2011

По моему мнению, используйте метод, который требует меньше вашего времени (что, вероятно, означает помещение #ifdefs в заголовочные файлы). Я не против, если компилятор должен работать усерднее, если мой полученный код будет чище. Если, возможно, вы работаете с многомиллионной строкой кода, которую вам постоянно приходится полностью перестраивать, возможно, дополнительная экономия того стоит. Но в большинстве случаев я подозреваю, что дополнительные расходы обычно не заметны.

6 голосов
/ 24 марта 2011

Продолжайте делать то, что вы делаете - это понятно, менее подвержено ошибкам и хорошо известно авторам компиляторов, поэтому не так неэффективно, как это могло быть десятилетие или два назад.

Вы можете использовать нестандартный #pragma once - Если вы ищете, вероятно, по крайней мере на полке стоит включить гвардию против прагмы после обсуждения, поэтому я не буду рекомендовать одно из другого.

4 голосов
/ 07 сентября 2015

Пайк написал еще об этом в https://talks.golang.org/2012/splash.article:

. В 1984 г. компиляция ps.c, источника команды Unix ps, наблюдалась #include <sys/stat.h> 37 развремя вся предварительная обработка была сделана.Хотя при этом содержимое отбрасывается 36 раз, большинство реализаций C открывают файл, читают его и сканируют все 37 раз.Фактически, без большой хитрости такое поведение требуется потенциально сложной макросемантикой препроцессора C.

Компиляторы стали достаточно умными, так как: https://gcc.gnu.org/onlinedocs/cppinternals/Guard-Macros.html,, так что это меньшевыпускать сейчас.

Создание одного двоичного файла C ++ в Google позволяет открывать и читать сотни отдельных заголовочных файлов десятки тысяч раз.В 2007 году инженеры по сборке в Google разработали сборку основного двоичного файла Google.Файл содержал около двух тысяч файлов, которые, если их просто объединить, составили 4,2 мегабайта.Ко времени расширения #include на вход компилятора было доставлено более 8 гигабайт, что привело к увеличению 2000 байт на каждый исходный байт C ++.

В качестве еще одной точки данных в 2003 году GoogleСистема сборки была перемещена из одного файла Makefile в дизайн для каждого каталога с более управляемыми, более явными зависимостями.Типичный двоичный файл сократился примерно на 40% по размеру файла, просто из-за более точной записи зависимостей.Несмотря на это, свойства C ++ (или C в этом отношении) делают нецелесообразным автоматическую проверку этих зависимостей, и сегодня у нас все еще нет точного понимания требований к зависимостям больших двоичных файлов Google C ++.

Вопрос о двоичных размерах все еще актуален.Компиляторы (линкеры) довольно консервативны в отношении удаления неиспользуемых символов. Как удалить неиспользуемые символы C / C ++ с помощью GCC и ld?

В Plan 9 было запрещено содержать дополнительные заголовочные файлы #include;все #includes должны были находиться в файле C верхнего уровня.Конечно, это требовало некоторой дисциплины - программист должен был перечислять необходимые зависимости ровно один раз, в правильном порядке - но документация помогла, и на практике это сработало очень хорошо.

Это возможное решение,Другая возможность состоит в том, чтобы иметь для вас инструмент, который управляет включениями, например, MakeDeps .

Также существуют сборки для единства, иногда называемые SCU, сборки для одного модуля компиляции.Для этого есть инструменты, такие как https://github.com/sakra/cotire

Использование системы сборки, которая оптимизирует скорость инкрементной компиляции, также может быть полезным.Я говорю о Google Bazel и подобных.Однако он не защищает вас от изменений в заголовочном файле, который включен в большое количество других файлов.

Наконец, в работе есть предложение для модулей C ++, замечательные вещи https://groups.google.com/a/isocpp.org/forum/#!forum/modules. См. Также Что такое модули C ++?

2 голосов
/ 24 марта 2011

То, как вы сейчас это делаете, является обычным способом. Метод Пайка немного сокращает время компиляции, но с современными компиляторами, вероятно, не очень сильно (когда Пайк писал свои заметки, компиляторы не были связаны с оптимизатором), он загромождает модули и подвержен ошибкам.

Вы все еще можете сократить множественное включение, не включая заголовки из заголовков, а вместо этого документируя их с помощью «include <foodefs.h> перед включением этого заголовка».

1 голос
/ 24 марта 2011

У него, возможно, был спор, когда он писал это.В наши дни достойные компиляторы достаточно умны, чтобы справиться с этим хорошо.

1 голос
/ 24 марта 2011

Рекомендую поместить их в сам исходный файл.Не нужно жаловаться на несколько тысяч ненужных проанализированных строк кода на реальных ПК.

Кроме того - гораздо больше работы и источника, если вы проверяете каждый отдельный заголовок в каждом исходном файле, который включает заголовок.

И вам придется обрабатывать заголовочные файлы, отличные от стандартных и других сторонних заголовков.

0 голосов
/ 24 марта 2011

Я согласен с вашим подходом - как прокомментировали другие, он более понятен, самодокументирован и требует меньшего количества обслуживания.

Моя теория о том, почему Роб Пайк мог предложить свой подход: он говорит о С, а не о С ++,

В C ++, если у вас много классов и вы объявляете каждый из них в своем собственном заголовочном файле, тогда у вас будет много заголовочных файлов.C на самом деле не предоставляет такого рода детализированную структуру (я не помню, чтобы я видел много одноструктурных заголовочных файлов C), а .h / .c file-pair имеют тенденцию к увеличению и содержаниючто-то вроде модуля или подсистемы.Итак, меньше заголовочных файлов.В этом случае подход Роба Пайка может сработать.Но я не считаю его подходящим для нетривиальных программ на C ++.

...