Почему мои защитники не препятствуют рекурсивному включению и множественным определениям символов?

Два распространенных вопроса о включают охранников:

  • ПЕРВЫЙ ВОПРОС:

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

    "хиджра"

    #ifndef A_H
    #define A_H
    
    #include "b.h"
    
    ...
    
    #endif // A_H
    

    "b.h"

    #ifndef B_H
    #define B_H
    
    #include "a.h"
    
    ...
    
    #endif // B_H
    

    "main.cpp"

    #include "a.h"
    int main()
    {
        ...
    }
    

    Почему возникает ошибка компиляции "main.cpp"? Что мне нужно сделать, чтобы решить мою проблему?


  1. ВТОРОЙ ВОПРОС:

    Почему не включены защитники, предотвращающие несколько определений? Например, когда мой проект содержит два файла, которые содержат один и тот же заголовок, иногда компоновщик жалуется, что некоторый символ определен несколько раз. Например:

    "header.h"

    #ifndef HEADER_H
    #define HEADER_H
    
    int f()
    {
        return 0;
    }
    
    #endif // HEADER_H
    

    "source1.cpp"

    #include "header.h"
    ...
    

    "source2.cpp"

    #include "header.h"
    ...
    

    Почему это происходит? Что мне нужно сделать, чтобы решить мою проблему?

Ответ 1

ПЕРВЫЙ ВОПРОС:

Почему не включить защитников моих файлов заголовков из взаимного, рекурсивного включения?

Они.

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

Предположим, что ваши взаимно включающие заголовочные файлы a.h и b.h имеют тривиальное содержимое, то есть эллипсы в разделах кода из текста вопроса заменяются пустой строкой. В этой ситуации ваш main.cpp будет с удовольствием компилироваться. И это только благодаря вашим охранникам!

Если вы не уверены, попробуйте удалить их:

//================================================
// a.h

#include "b.h"

//================================================
// b.h

#include "a.h"

//================================================
// main.cpp
//
// Good luck getting this to compile...

#include "a.h"
int main()
{
    ...
}

Вы заметите, что компилятор сообщит об ошибке, когда достигнет предела глубины включения. Этот предел специфичен для реализации. В соответствии с пунктом 16.2/6 стандарта С++ 11:

В исходный файл, который был прочитан из-за директивы #include в другом файле, может отображаться директива preinccessing #include, до определенного предела реализации вложения.

Итак, что происходит?

  • При анализе main.cpp препроцессор будет соответствовать директиве #include "a.h". Эта директива сообщает препроцессору обрабатывать файл заголовка a.h, принимать результат этой обработки и заменять строку #include "a.h" на этот результат;
  • При обработке a.h препроцессор будет соответствовать директиве #include "b.h", и применяется тот же механизм: препроцессор обрабатывает файл заголовка b.h, принимает результат его обработки и заменяет директиву #include с этим результатом;
  • При обработке b.h директива #include "a.h" сообщает препроцессору обрабатывать a.h и заменяет эту директиву результатом;
  • Препроцессор снова начнет синтаксический анализ a.h, снова встретится с директивой #include "b.h", и это создаст потенциально бесконечный рекурсивный процесс. Достигнув критического уровня вложенности, компилятор сообщит об ошибке.

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

  • (аналогично предыдущему) При анализе main.cpp препроцессор будет соответствовать директиве #include "a.h". Это говорит препроцессору обрабатывать файл заголовка a.h, принимать результат этой обработки и заменять строку #include "a.h" на этот результат;
  • При обработке a.h препроцессор будет соответствовать директиве #ifndef A_H. Поскольку макрос A_H еще не определен, он будет продолжать обрабатывать следующий текст. Следующая директива (#defines A_H) определяет макрос A_H. Затем препроцессор будет соответствовать директиве #include "b.h": препроцессор должен обработать заголовочный файл b.h, взять результат его обработки и заменить директиву #include этим результатом;
  • При обработке b.h препроцессор будет соответствовать директиве #ifndef B_H. Поскольку макрос B_H еще не определен, он будет обрабатывать следующий текст. Следующая директива (#defines B_H) определяет макрос B_H. Затем директива #include "a.h" сообщит препроцессору обрабатывать a.h и заменить директиву #include в b.h результатом предварительной обработки a.h;
  • Компилятор снова начнет предварительную обработку a.h и снова встретится с директивой #ifndef A_H. Однако во время предыдущей предварительной обработки был определен макрос A_H. Поэтому на этот раз компилятор пропустит следующий текст до тех пор, пока не будет найдена соответствующая директива #endif, а выход этой обработки будет пустой строкой (если, конечно, ничего не следует директиве #endif). Поэтому препроцессор заменяет директиву #include "a.h" в b.h пустой строкой и будет отслеживать выполнение до тех пор, пока она не заменит исходную директиву #include в main.cpp.

Таким образом, включить защитники защищают от взаимного включения. Однако они не могут помочь с зависимостями между определениями ваших классов во взаимно включающих файлах:

//================================================
// a.h

#ifndef A_H
#define A_H

#include "b.h"

struct A
{
};

#endif // A_H

//================================================
// b.h

#ifndef B_H
#define B_H

#include "a.h"

struct B
{
    A* pA;
};

#endif // B_H

//================================================
// main.cpp
//
// Good luck getting this to compile...

#include "a.h"
int main()
{
    ...
}

Учитывая указанные выше заголовки, main.cpp не будет компилироваться.

Почему это происходит?

Чтобы узнать, что происходит, достаточно повторить шаги 1-4.

Легко видеть, что первые три шага и большая часть четвертого шага не подвержены этому изменению (просто прочитайте их, чтобы убедиться). Однако в конце шага 4 происходит что-то другое: после замены директивы #include "a.h" в b.h пустой строкой препроцессор начнёт синтаксический анализ содержимого b.h и, в частности, определение B, К сожалению, в определении B упоминается класс A, который никогда не выполнялся раньше именно из-за защит включения!

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

Что мне нужно сделать, чтобы решить мою проблему?

Вам нужно передовые объявления.

На самом деле определение класса A не требуется для определения класса B, поскольку указатель на A объявляется как переменная-член, а не объект типа A. Поскольку указатели имеют фиксированный размер, компилятору не требуется знать точное расположение A и не вычислять его размер, чтобы правильно определить класс B. Следовательно, достаточно forward-declare class A в b.h и сообщить компилятору о его существовании:

//================================================
// b.h

#ifndef B_H
#define B_H

// Forward declaration of A: no need to #include "a.h"
struct A;

struct B
{
    A* pA;
};

#endif // B_H

Теперь ваш main.cpp будет компилироваться. Несколько замечаний:

  • Не удалось полностью исключить взаимное включение, заменив директиву #include на декларацию forward в b.h, чтобы эффективно выразить зависимость B от A: использование передовых объявлений, когда это возможно/практично, также рассматривается быть хорошей практикой программирования, поскольку она помогает избежать ненужных включений, тем самым уменьшая общее время компиляции. Однако после устранения взаимного включения main.cpp нужно будет изменить на #include как a.h, так и b.h (если последнее вообще необходимо), потому что b.h не косвенно #include d через a.h;
  • В то время как переднего объявления класса A достаточно, чтобы компилятор мог указать указатели на этот класс (или использовать его в любом другом контексте, где допустимы неполные типы), указатели разыменования на A (например, для вызова функция-член) или вычисление его размера являются незаконными операциями над неполными типами: если это необходимо, полное компиляционное определение A должно быть доступно компилятору, что означает, что должен быть указан файл заголовка, который определяет его. Вот почему определения классов и реализация их функций-членов обычно разделяются на заголовочный файл и файл реализации для этого класса (шаблоны классов являются исключением из этого правила): файлы реализации, которые никогда не являются #include d другими файлами в проекте может безопасно #include все необходимые заголовки, чтобы сделать видимыми определения. С другой стороны, файлы заголовков не будут #include других файлов заголовков, если они действительно не нуждаются в этом (например, чтобы сделать определение базового класса видимым) и будут использовать форвардные объявления, когда это возможно/практично.

ВТОРОЙ ВОПРОС:

Почему не включены защитники, предотвращающие несколько определений?

Они.

То, что они не защищают вас, это несколько определений в отдельных единицах перевода. Это также объясняется в этом Q & A в StackOverflow.

Сказав это, попробуйте удалить включенные защитные устройства и скомпилировать следующую модифицированную версию source1.cpp (или source2.cpp, для чего это важно):

//================================================
// source1.cpp
//
// Good luck getting this to compile...

#include "header.h"
#include "header.h"

int main()
{
    ...
}

Компилятор, безусловно, будет жаловаться здесь на переопределение f(). Это очевидно: его определение включается дважды! Однако выше source1.cpp будет скомпилировано без проблем, если header.h содержит соответствующие атрибуты include. Это ожидалось.

Тем не менее, даже если присутствуют защитные устройства include и компилятор перестанет беспокоить вас сообщением об ошибке, компоновщик будет настаивать на том, что при объединении объектного кода, полученного из компиляции source1.cpp и source2.cpp и откажется генерировать ваш исполняемый файл.

Почему это происходит?

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

Фактически, компиляция проекта с n единицами перевода (.cpp files) похожа на выполнение одной и той же программы (компилятор) n раз, каждый раз с другим вводом: разные исполнения одной и той же программы не будет делиться состоянием предыдущего выполнения программы. Таким образом, каждый перевод выполняется независимо, и символы препроцессора, встречающиеся при компиляции одной единицы перевода, не будут запоминаться при компиляции других единиц перевода (если вы думаете об этом на мгновение, вы легко поймете, что это действительно желаемое поведение).

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

Тем не менее, при объединении объектного кода, сгенерированного из компиляции всех файлов .cpp вашего проекта, компоновщик увидит, что один и тот же символ определен более одного раза, и поскольку это нарушает Одно правило определения. В пункте 3.2/3 стандарта С++ 11:

Каждая программа должна содержать ровно одно определение каждой функции non-inline, которая является odr-используемой в этой программе; не требуется диагностика. Определение может явно отображаться в программе, оно может быть найдено в стандартной или определяемой пользователем библиотеке или (если необходимо), оно неявно определено (см. 12.1, 12.4 и 12.8). Встроенная функция должна быть определена в каждой единицы перевода, в которой она используется.

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

Что мне нужно сделать, чтобы решить мою проблему?

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

В противном случае вам нужно сохранить только объявление своей функции в header.h, поставив его определение (тело) только в один отдельный файл .cpp (это классический подход).

Ключевое слово inline представляет собой необязательный запрос компилятору, чтобы встроить тело функции непосредственно на сайт вызова, вместо того, чтобы настраивать кадр стека для регулярного вызова функции. Хотя компилятор не должен выполнять ваш запрос, ключевому слову inline удается сообщить компоновщику, чтобы он допускал множественные определения символов. Согласно пункту 3.2/5 стандарта С++ 11:

Может быть более одного определения типа класса (раздел 9), типа перечисления (7.2), встроенной функции с внешней связью (7.1.2), шаблон класса (раздел 14), шаблон нестатической функции (14.5.6), элемент статических данных шаблона класса (14.5.1.3), функция-член шаблона класса (14.5.1.1) или специализированная специализация шаблона, для которой какой-либо шаблон параметры не указаны (14.7, 14.5.5) в программе, при условии, что каждое определение отображается в другой единицы перевода и при условии, что определения удовлетворяют следующим требованиям [...]

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

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

Альтернативным способом достижения того же результата, что и с ключевым словом static, является функция function f() в неназванном пространстве имен. В параграфе 3.5/4 стандарта С++ 11:

Неименованное пространство имен или пространство имен, объявленное прямо или косвенно в неназванном пространстве имен, имеет внутреннюю связь. Все остальные пространства имен имеют внешнюю связь. Имя с областью пространства имен, которая не была предоставлена ​​внутренней связью выше, имеет ту же связь, что и охватывающее пространство имен, если это имя:

- переменная; или

- функция; или

- именованный класс (раздел 9) или неназванный класс, определенный в объявлении typedef, в котором класс имеет имя typedef для целей привязки (7.1.3); или

- именованное перечисление (7.2) или неназванное перечисление, определенное в объявлении typedef, в котором перечисление имеет имя typedef для целей привязки (7.1.3); или

- перечислитель, относящийся к перечислению с привязкой; или

- шаблон.

По той же причине, указанной выше, ключевое слово inline должно быть предпочтительным.

Ответ 3

Прежде всего, вы должны быть на 100% уверены, что у вас нет дубликатов в "include guard".

С помощью этой команды

grep -rh "#ifndef" * 2>&1 | uniq -c | sort -rn | awk '{print $1 " " $3}' | grep -v "^1\ "

вы 1) выделите все включенные охранники, получите уникальную строку со счетчиком для каждого включаемого имени, отсортируете результаты, напечатаете только счетчик и включите имя и удалите те, которые действительно уникальны.

СОВЕТ: это эквивалентно получению списка дублированных имен включений