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

Два распространенных вопроса include охранников :

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

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

    «Ах»

    #ifndef A_H #define A_H #include "bh" ... #endif // A_H 

    «ЧД»

     #ifndef B_H #define B_H #include "ah" ... #endif // B_H 

    “Main.cpp”

     #include "ah" int main() { ... } 

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


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

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

    “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" ... 

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

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

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

    Они есть .

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

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

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

     //================================================ // ah #include "bh" //================================================ // bh #include "ah" //================================================ // main.cpp // // Good luck getting this to compile... #include "ah" int main() { ... } 

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

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

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

    1. При parsingе main.cpp препроцессор будет соответствовать директиве #include "ah" . Эта директива сообщает препроцессору обрабатывать файл заголовка ah , принимать результат этой обработки и заменять строку #include "ah" на этот результат;
    2. При обработке ah препроцессор будет соответствовать директиве #include "bh" , и применяется тот же механизм: препроцессор обрабатывает файл заголовка bh , принимает результат его обработки и заменяет директиву #include этим результатом;
    3. При обработке bh директива #include "ah" сообщает препроцессору обрабатывать ah и заменять эту директиву результатом;
    4. Препроцессор снова начнет синтаксический анализ ah снова встретит директиву #include "bh" , и это создаст потенциально бесконечный рекурсивный процесс. Достигнув критического уровня вложенности, компилятор сообщит об ошибке.

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

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

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

     //================================================ // ah #ifndef A_H #define A_H #include "bh" struct A { }; #endif // A_H //================================================ // bh #ifndef B_H #define B_H #include "ah" struct B { A* pA; }; #endif // B_H //================================================ // main.cpp // // Good luck getting this to compile... #include "ah" int main() { ... } 

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

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

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

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

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

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

    Вам нужны форвардные декларации .

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

     //================================================ // bh #ifndef B_H #define B_H // Forward declaration of A: no need to #include "ah" struct A; struct B { A* pA; }; #endif // B_H 

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

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

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

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

    Они есть .

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

    Слишком это видно, попробуйте удалить защитные source1.cpp include и скомпилировать следующую модифицированную версию 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 содержит надлежащие header.h защитные устройства . Это ожидалось.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    – Переменная; или

    функция ; или

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

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

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

    – шаблон.

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

    Давайте будем гением компьютера.