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

Два общих вопроса о включать охранников:

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

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

    «Хиджра»

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

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

65

Решение

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

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

Они есть.

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

Предположим, что вы в том числе 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 стандарта C ++ 11:

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

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

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

Когда включают охрану присутствуют, однако на шаге 4 не будет настроена бесконечная рекурсия. Посмотрим, почему:

  1. (так же, как и раньше) При разборе main.cppпрепроцессор будет соответствовать директиве #include "a.h", Это говорит препроцессору обрабатывать заголовочный файл a.h, возьмите результат этой обработки и замените строку #include "a.h" с этим результатом;
  2. Во время обработки a.hпрепроцессор будет соответствовать директиве #ifndef A_H, Поскольку макрос A_H еще не определен, он продолжит обработку следующего текста. Последующая директива (#defines A_H) определяет макрос A_H, Затем препроцессор встретит директиву #include "b.h": препроцессор теперь должен обрабатывать заголовочный файл b.h, возьмите результат его обработки и замените #include директива с этим результатом;
  3. При обработке b.hпрепроцессор будет соответствовать директиве #ifndef B_H, Поскольку макрос B_H еще не определен, он продолжит обработку следующего текста. Последующая директива (#defines B_H) определяет макрос B_H, Затем директива #include "a.h" скажет препроцессору обрабатывать a.h и заменить #include директива в b.h с результатом предварительной обработки a.h;
  4. Компилятор начнет предварительную обработку 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, Следовательно, достаточно вперед объявить учебный класс 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 сейчас обязательно скомпилирую. Пара замечаний:

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

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

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

Они есть.

То, что они не защищают вас от нескольких определений в отдельных переводческих единицах. Это также объясняется в этот вопрос& на 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 содержит надлежащую охрану. Это ожидается.

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

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

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

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

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

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

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

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

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

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

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

inline Ключевое слово представляет необязательный запрос к компилятору, чтобы встроить тело функции непосредственно на сайт вызова, вместо того, чтобы устанавливать фрейм стека для обычного вызова функции. Хотя компилятор не должен выполнять ваш запрос, inline Ключевое слово действительно успешно говорит компоновщику допускать множественные определения символов. Согласно пункту 3.2 / 5 стандарта C ++ 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 Ключевое слово, чтобы поставить функцию f() в безымянное пространство имен. Согласно пункту 3.5 / 4 стандарта C ++ 11:

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

— Переменная; или же

функция; или же

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

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

— перечислитель, принадлежащий перечислению со связью; или же

Шаблон.

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

123

Другие решения

Прежде всего, вы должны быть на 100% уверены, что у вас нет дубликатов в «Включить охрану».

С этой командой

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

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

ПОДСКАЗКА: это эквивалентно получению списка дублированных имен включений

-1

По вопросам рекламы ammmcru@yandex.ru
Adblock
detector