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

Я недавно отправил вопрос, просящий то, какие действия составят Дзэн C++. Я получил превосходные ответы, но я не мог понять одну рекомендацию:

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

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

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

Править: Я нашел этот вопрос, который обращается к моей части "Лучших практик".

24
задан Community 23 May 2017 в 10:30
поделиться

7 ответов

Самодостаточный файл заголовка - это файл, который для правильной работы не зависит от контекста, в который он включен. Если вы убедитесь, что вы #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 ] без первого #including. Чтобы это работало, в MyClass.cpp вам нужно поместить #include перед #include «MyClass.h» .
Если пользователь MyClass не сможет этого сделать, он получит сообщение об ошибке, что std :: string не включен .

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

33
ответ дан 28 November 2019 в 22:33
поделиться

You'd want to use the method described in the GNU C Preprocessor Manual:

2.4 Once-Only Headers

If a header file happens to be included twice, the compiler will process its contents twice. This is very likely to cause an error, e.g. when the compiler sees the same structure definition twice. Even if it does not, it will certainly waste time.

The standard way to prevent this is to enclose the entire real contents of the file in a conditional, like this:

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

the entire file

#endif /* !FILE_FOO_SEEN */

This construct is commonly known as a wrapper #ifndef. When the header is included again, the conditional will be false, because FILE_FOO_SEEN is defined. The preprocessor will skip over the entire contents of the file, and the compiler will not see it twice.

CPP optimizes even further. It remembers when a header file has a wrapper ‘#ifndef’. If a subsequent ‘#include’ specifies that header, and the macro in the ‘#ifndef’ is still defined, it does not bother to rescan the file at all.

You can put comments outside the wrapper. They will not interfere with this optimization.

The macro FILE_FOO_SEEN is called the controlling macro or guard macro. In a user header file, the macro name should not begin with ‘_’. In a system header file, it should begin with ‘__’ to avoid conflicts with user programs. In any kind of header file, the macro name should contain the name of the file and some additional text, to avoid conflicts with other header files.

1
ответ дан 28 November 2019 в 22:33
поделиться

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

Спасибо jalf за исправление. Из Википедии

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

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

1
ответ дан 28 November 2019 в 22:33
поделиться

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

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

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

Существует очень простой способ гарантировать, что заголовок является автономным. In the source file, the first header you include is the module's header. If it compiles like this, the header is self-contained (self-sufficient). If it does not, fix the header until it is (reliably1) self-contained.

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 */

Almost all headers should be protected against multiple inclusion. (The standard header is an explicit exception to the rule — hence the 'almost' qualifier.)

perverse.c

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

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

Note that even though it was traditionally considered a good idea to include the standard headers before the project headers, in this case, it is crucial to the testability that the module header (perverse.h) comes before all others. The only exception I'd allow is including a configuration header ahead of the module header; however, even that is dubious. If the module header needs to use (or maybe just 'can use') the information from the configuration header, it should probably include the configuration header itself, rather than rely on the source files using it to do so. However, if you need to configure which version of POSIX to request support for, that must be done before the first system header is included.


Footnote 1: Steve Jessop's comment to Shoosh's answer is why I put the parenthesized '(reliably)' comment into my 'fix it' comment. He said:

Another factor making this difficult is the "system headers can include other headers" rule in C++. If includes , then it's quite difficult to discover that you've forgotten to include in some header which does [not] use [or ]. Compiling the header on its own gives no errors: it's self-sufficient on this version of your compiler, but on another compiler it might not work.

See also the answer by Toby Speight about IWYU — Include What You Use.


Appendix: Matching these rules with GCC Precompiled Headers

The GCC rules for precompiled headers permit just one such header per translation unit, and it must appear before any C tokens.

GCC 4.4.1 Manual, §3.20 Using Precompiled Headers

A precompiled header file can be used only when these conditions apply:

  • Only one precompiled header can be used in a particular compilation.
  • A precompiled header can’t be used once the first C token is seen. You can have директивы препроцессора перед предварительно скомпилированным заголовком; вы даже можете включить предварительно скомпилированный заголовок из другого заголовка, если нет токенов C перед #include.
  • [...]
  • Любые макросы, определенные до включения предварительно скомпилированного заголовка, должны быть определены таким же образом, как когда был создан предварительно скомпилированный заголовок, или не должен влиять на предварительно скомпилированный заголовок, что обычно означает, что они не отображаются в предварительно скомпилированном заголовок вообще.

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

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

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

chkhdr script

Я использую этот chkhdr скрипт для проверки самодостаточности заголовков. Хотя в shebang написано «оболочка Korn», код на самом деле подходит для 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 "$@"
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 доступен через Интернет-архив

Указанный выше URL-адрес больше не работает (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 в заголовках?

21
ответ дан 28 November 2019 в 22:33
поделиться

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

5
ответ дан 28 November 2019 в 22:33
поделиться

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

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

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

3
ответ дан 28 November 2019 в 22:33
поделиться

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

#ifndef MY_PROTECTED_HEADER_H
#define MY_PROTECTED_HEADER_H
/*
 * Stuff here
 */
#endif /* MY_PROTECTED_HEADER_H */
0
ответ дан 28 November 2019 в 22:33
поделиться
Другие вопросы по тегам:

Похожие вопросы: