Ответ 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
должно быть предпочтительным.