Подтвердить что ты не робот

Самодостаточные файлы заголовков в C/С++

Недавно я опубликовал вопрос о том, какие действия будут представлять собой Zen of С++. Я получил отличные ответы, но я не мог понять одну рекомендацию:

  • Сделать заголовочные файлы самодостаточными

Как вы гарантируете, что ваши файлы заголовков являются самодостаточными?

Любые другие рекомендации или передовая практика, связанные с проектированием и реализацией файлов заголовков в C/С++, будут приветствоваться.

Изменить: я нашел этот вопрос, в котором рассматривается моя "Практическая практика".

4b9b3361

Ответ 1

Самодостаточный заголовочный файл - это файл, который не зависит от контекста того, где он включен, для правильной работы. Если вы обязательно #include или определите/объявите все, прежде чем использовать его, у вас есть самодостаточный заголовок.
Примером несамостоятельного заголовка может быть что-то вроде этого:

----- MyClass.h -----

class MyClass
{
   MyClass(std::string s);
};

-

---- MyClass.cpp -----

#include <string>
#include "MyClass.h"

MyClass::MyClass(std::string s)
{}

В этом примере MyClass.h использует std::string без первого #include. Чтобы это работало, в MyClass.cpp необходимо поставить #include <string> перед #include "MyClass.h".
Если пользователь MyClass не сможет сделать это, он получит ошибку, что std :: string не включен.

Поддержание ваших заголовков, чтобы быть самодостаточным, часто можно пренебречь. Например, у вас есть огромный заголовок MyClass, и вы добавляете к нему еще один небольшой метод, который использует std :: string. Во всех местах, где этот класс в настоящее время используется, он уже #include до MyClass.h. затем однажды вы #include MyClass.h в качестве первого заголовка, и вдруг у вас есть все эти новые ошибки в файле, который вы даже не коснулись (MyClass.h)
Тщательное поддержание ваших заголовков, чтобы быть самодостаточным, поможет избежать этой проблемы.

Ответ 2

НАСА Центр космических полетов им. Годдарда (GSFC) опубликовало стандарты программирования C и C++ для решения этой проблемы.

Предположим, у вас есть модуль с исходным файлом perverse.c и его заголовком perverse.h.

Обеспечение автономности заголовка

Существует очень простой способ обеспечить автономность заголовка. В исходном файле первый заголовок, который вы включаете, является заголовком модуля. Если он компилируется следующим образом, заголовок самодостаточен (самодостаточен). Если это не так, исправьте заголовок, пока он (надежно 1) не будет автономным.

perverse.h

#ifndef PERVERSE_H_INCLUDED
#define PERVERSE_H_INCLUDED

#include <stddef.h>

extern size_t perverse(const unsigned char *bytes, size_t nbytes);

#endif /* PERVERSE_H_INCLUDED */

Почти все заголовки должны быть защищены от многократного включения. (Стандартный заголовок <assert.h> является явным исключением из правила - отсюда и квалификатор "почти".)

perverse.c

#include "perverse.h"
#include <stdio.h>   // defines size_t too

size_t perverse(const unsigned char *bytes, size_t nbytes)
{
    ...etc...
}

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


Сноска 1: Стив Джессопкомментирует на Shooshответ, поэтому я вставил заключенный в скобки комментарий (надежно) в свой комментарий "исправить это". Он сказал:

Другим фактором, усложняющим это, является правило "системные заголовки могут включать другие заголовки" в C++. Если <iostream> включает в себя <string>, то довольно сложно обнаружить, что вы забыли включить <string> в заголовок, который [не] использует <iostream> [или <string>]. Сам по себе компиляция заголовка не дает ошибок: он самодостаточен в этой версии вашего компилятора, но в другом компиляторе он может не работать.

См. также ответ Тоби Спейта об IWYU - укажите, что вы используете.


Приложение. Сопоставление этих правил с предварительно скомпилированными заголовками GCC

Правила GCC для предварительно скомпилированных заголовков разрешают только один такой заголовок на единицу перевода, и он должен появляться перед любыми токенами C.

GCC 4.4.1 Руководство, §3.20 Использование предварительно скомпилированных заголовков

Предварительно скомпилированный заголовочный файл можно использовать только при соблюдении следующих условий:

  • В одной компиляции можно использовать только один предварительно скомпилированный заголовок.
  • Предварительно скомпилированный заголовок не может быть использован после того, как будет замечен первый C-токен. Вы можете иметь директивы препроцессора перед предварительно скомпилированным заголовком; Вы даже можете включить предварительно скомпилированный заголовок из другого заголовка, если перед #include нет токенов C.
  • [...]
  • Любые макросы, определенные до включения предварительно скомпилированного заголовка, должны быть определены так же, как когда был скомпилирован заголовок, или не должен влиять на предварительно скомпилированный заголовок, что обычно означает, что они не отображаются в предварительно скомпилированном заголовке заголовок на всех.

В первом приближении эти ограничения означают, что предварительно скомпилированный заголовок должен быть первым в файле. Во втором приближении отмечается, что если "config.h" содержит только операторы #define, он может появиться перед предварительно скомпилированным заголовком, но гораздо более вероятно, что (a) определения из config.h влияют на остальную часть кода, и (b) предварительно скомпилированный заголовок должен в любом случае включать config.h.

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

Учитывая различие требований между руководящими принципами GSFC и предварительно скомпилированными заголовками (и при условии, что используются предварительно скомпилированные заголовки), я думаю, что я бы обеспечил автономность и идемпотентность заголовков, используя отдельный механизм. Я уже делаю это для основных проектов, над которыми я работаю - реорганизация заголовков в соответствии с рекомендациями GSFC - нелегкий вариант, и я использую скрипт chkhdr, показанный ниже. Вы даже можете сделать это как шаг "сборки" в каталоге заголовков - убедитесь, что все заголовки являются автономными как правило "компиляции".

сценарий чхдр

Я использую этот скрипт chkhdr для проверки автономности заголовков. Хотя шебанг говорит "оболочка Корна", код на самом деле в порядке с Bash или даже с оригинальной (System V-ish) Bourne Shell.

#!/bin/ksh
#
# @(#)$Id: chkhdr.sh,v 1.2 2010/04/24 16:52:59 jleffler Exp $
#
# Check whether a header can be compiled standalone

tmp=chkhdr-$$
trap 'rm -f $tmp.?; exit 1' 0 1 2 3 13 15

cat >$tmp.c <<EOF
#include HEADER /* Check self-containment */
#include HEADER /* Check idempotency */
int main(void){return 0;}
EOF

options=
for file in "[email protected]"
do
    case "$file" in
    (-*)    options="$options $file";;
    (*)     echo "$file:"
            gcc $options -DHEADER="\"$file\"" -c $tmp.c
            ;;
    esac
done

rm -f $tmp.?
trap 0

Так получилось, что мне никогда не приходилось передавать в скрипт какие-либо параметры, содержащие пробелы, поэтому код не обрабатывает параметры пробелов. Обработка их в оболочке Bourne/Korn, по крайней мере, делает сценарий более сложным и бесполезным; использование Bash и массива может быть лучше.

Использование:

chkhdr -Wstrict-prototypes -DULTRA_TURBO -I$PROJECT/include header1.h header2.h

Стандарт GSFC доступен через интернет-архив

Ссылка, указанная выше, больше не работает (404). Стандарт C++ (582-2003-004) можно найти на EverySpec.com (на странице 2); стандарт C (582-2000-005), по-видимому, отсутствует в действии.

Однако доступ к указанному стандарту кодирования NASA C можно получить и загрузить через интернет-архив:

http://web.archive.org/web/20090412090730/http://software.gsfc.nasa.gov/assetsbytype.cfm?TypeAsset=Standard

Смотрите также: Должен ли я использовать #include в заголовках?

Ответ 3

Убедитесь, что вы включили в заголовок все, что вам нужно, вместо того, чтобы предположить, что что-то, что вы включили, включает в себя что-то еще, что вам нужно.

Ответ 4

Старый вопрос, новый ответ.: -)

Теперь есть инструмент под названием include-what-you-use, который предназначен для анализа вашего кода именно для такого рода проблем. В Debian и производных системах его можно установить как пакет iwyu.

Ответ 5

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

Идея состоит в том, что если вам нужно добавить объект foo в ваш класс, вам просто нужно включить #include foo.h, и вам не нужно будет bar.h перед ним, чтобы получить foo.h для компиляции (например, есть вызов в foo, который возвращает экземпляр объекта bar. Возможно, вас не интересует этот вызов, но вам нужно будет добавить bar.h, чтобы компилятор знал, на что ссылается).

Я не уверен, что всегда соглашусь с этим советом. У большого проекта будут сотни файлов заголовков, и компиляция закончит чтение через общие из них сотни раз, чтобы игнорировать #ifdefs. То, что я видел в этом случае, является заголовочным файлом файлов заголовков, который является стандартным для проекта и включает тридцать общих. Он всегда первый в списке включений. Это может ускорить время компиляции, но делает обслуживание общего заголовка квалифицированной задачей.

Ответ 6

Вы хотите использовать метод, описанный в Руководство для препроцессора GNU C:

2.4 Одноразовые заголовки

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

Стандартный способ предотвратить это состоит в том, чтобы вложить все реальное содержимое файла в условное выражение, например:

/* File foo.  */
#ifndef FILE_FOO_SEEN
#define FILE_FOO_SEEN

весь файл

#endif /* !FILE_FOO_SEEN */

Эта конструкция широко известна как обертка #ifndef. Когда заголовок будет включен снова, условие будет ложным, потому что определено FILE_FOO_SEEN. Препроцессор пропустит все содержимое файла, а компилятор не увидит его дважды.

CPP оптимизирует еще больше. Он запоминает, когда заголовочный файл имеет обертку '#ifndef. Если последующий '#include указывает этот заголовок, а макрос в' #ifndef все еще определен, он вообще не пытается повторно просмотреть файл.

Вы можете размещать комментарии вне оболочки. Они не будут мешать этой оптимизации.

Макрос FILE_FOO_SEEN называется управляющим макросом или защитным макросом. В файле заголовка пользователя имя макроса не должно начинаться с '_. В системном заголовочном файле он должен начинаться с '__, чтобы избежать конфликтов с пользовательскими программами. В любом виде файла заголовка имя макроса должно содержать имя файла и некоторый дополнительный текст, чтобы избежать конфликтов с другими файлами заголовков.

Ответ 7

Это отличный вопрос. Я думаю, что я буду пересматривать практику размещения stdafx.h в качестве первого включить в каждый .cpp файл при использовании Visual Studio. Если вы используете предварительно скомпилированные файлы заголовков, он все равно не имеет значения, может также иметь более дружественные файлы заголовков.

Спасибо за исправление. Из Wikipedia

Visual С++ не скомпилирует ничего перед #include "stdafx.h" в исходном файле, если только параметр компиляции /Yu 'stdafx.h ' unchecked (по умолчанию); он принимает все код в источнике до и включительно эта строка уже скомпилирована.

Итак, это означает, что предварительно скомпилированные заголовки нарушают самодостаточное правило заголовка, правильно?

Ответ 8

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

#ifndef MY_PROTECTED_HEADER_H
#define MY_PROTECTED_HEADER_H
/*
 * Stuff here
 */
#endif /* MY_PROTECTED_HEADER_H */