Является ли хорошей практикой полагаться на транзитные заголовки?


38

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

Вот пример Entity.hpp:

#include "RenderObject.hpp"
#include "Texture.hpp"

struct Entity {
    Texture texture;
    RenderObject render();
}

(Предположим, что предварительное объявление для RenderObjectне вариант.)

Теперь я знаю, что это RenderObject.hppвключает Texture.hpp- я знаю это, потому что у каждого RenderObjectесть Textureчлен. Тем не менее, я явно включаю Texture.hppв Entity.hpp, потому что я не уверен, если это хорошая идея, чтобы полагаться на то, что он включен в RenderObject.hpp.

Итак: это хорошая практика или нет?


19
Где в вашем примере включены охранники? Вы просто забыли их случайно, я надеюсь?
Док Браун

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

Вот почему есть #ifndef _RENDER_H #define _RENDER_H ... #endif.
Сампатрисрис

@ Думаю, ты неправильно понял проблему. С одним из его предложений этого не должно произойти.
Mooing Duck

1
@DocBrown, #pragma onceрешает вопрос, нет?
Pacerier

Ответы:


65

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

Причины:

  • Это дает понять разработчикам, которые читают исходный код именно то, что требуется для рассматриваемого исходного файла. Здесь кто-то, глядя на первые несколько строк в файле, может увидеть, что вы имеете дело с Textureобъектами в этом файле.
  • Это позволяет избежать проблем, когда измененные заголовки вызывают проблемы компиляции, когда им больше не нужны конкретные заголовки. Например, предположим, вы понимаете, что на RenderObject.hppсамом деле не нуждается в Texture.hppсебе.

Следствие состоит в том, что вы никогда не должны включать заголовок в другой заголовок, если он явно не нужен в этом файле.


10
Согласитесь с следствием - при условии, что ВСЕГДА следует включать другой заголовок, если он в этом нуждается!
Андрей

1
Мне не нравится практика прямого включения заголовков для всех отдельных классов. Я за кумулятивные заголовки. То есть, я думаю, что файл высокого уровня должен ссылаться на «модуль» какого-то вида, который он использует, но не должен напрямую включать все отдельные части.
edA-qa mort-ora-y

8
Это приводит к появлению больших монолитных заголовков, включаемых в каждый файл, даже если им нужно только небольшое количество того, что находится в нем, что приводит к увеличению времени компиляции и усложняет рефакторинг.
Gort the Robot

6
Google разработал инструмент, который поможет обеспечить соблюдение именно этого совета, который называется « включай, что ты используешь» .
Мэтью Г.

3
Основная проблема времени компиляции с большими монолитными заголовками - не время компиляции самого кода заголовка, а необходимость компиляции каждого файла cpp в вашем приложении каждый раз, когда этот заголовок изменяется. Прекомпилированные заголовки не помогают этому.
Gort the Robot

23

Общее правило: включите то, что вы используете. Если вы используете объект напрямую, то включите его заголовочный файл напрямую. Если вы используете объект A, который использует B, но не используете B самостоятельно, включите только Ah

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


10

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

Да.

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

У нас есть охрана заголовков, чтобы гарантировать, что двойное включение не вредно.


3

Мнения расходятся по этому вопросу, но я считаю, что каждый файл (будь то исходный файл c / cpp или заголовочный файл h / hpp) должен иметь возможность компилироваться или анализироваться самостоятельно.

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

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

С другой стороны, не имеет значения (как правило), если вы #include файл, который вам не нужен ...


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


Примечание о порядке включения: иногда важен порядок, например, при включении заголовков X11. Это может быть связано с дизайном (который в этом случае может считаться плохим дизайном), иногда это связано с проблемами несовместимости.
Hyde

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

2

Это зависит от того, является ли это транзитивное включение необходимостью (например, базовым классом) или из-за деталей реализации (закрытый член).

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

Пример: Ah включен в Bh, который используется C.cpp. Если Bh использовал Ah для некоторых деталей реализации, то C.cpp не должен предполагать, что Bh продолжит делать это. Но если Bh использует Ah для базового класса, то C.cpp может предположить, что Bh продолжит включать соответствующие заголовки для своих базовых классов.

Вы видите здесь фактическое преимущество НЕ дублирования включений заголовка. Скажем, что базовый класс, используемый Bh, на самом деле не принадлежит классу Ah и преобразован в сам Bh. Bh теперь автономный заголовок. Если C.cpp избыточно включил Ah, теперь он включает ненужный заголовок.


2

Может быть другой случай: у вас есть Ah, Bh и ваш C.cpp, Bh включает Ah

так что в C.cpp вы можете написать

#include "B.h"
#include "A.h" // < this can be optional as B.h already has all the stuff in A.h

Так что, если вы не напишите здесь #include «Ах», что может произойти? в вашем C.cpp используются A и B (например, класс). Позже вы изменили свой код C.cpp, удалили вещи, связанные с B, но оставили Bh включенным там.

Если вы включите и Ah, и Bh, а теперь на этом этапе, инструменты, которые обнаруживают ненужные включения, могут помочь вам указать, что включение Bh больше не требуется. Если вы включаете только Bh, как указано выше, то инструментам / людям трудно обнаружить ненужные включения после изменения кода.


1

Я придерживаюсь слегка отличающегося подхода от предложенных ответов.

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

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

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

Несмотря на то, что дизайн программного обеспечения Large Scale C ++ (от Джона Лакоса) несколько устарел, все это подробно объясняется.


1
Не согласен с этой стратегией ... если вы включите заголовочный файл в исходный файл, вам придется отслеживать все его зависимости. Лучше включить напрямую, чем пытаться документировать список!
Андрей

@ Andrew есть инструменты и скрипты для проверки того, что и сколько раз включено.
BЈовић

1
Я заметил оптимизацию на некоторых из последних компиляторов, чтобы справиться с этим. Они распознают типичное охранное заявление и обрабатывают его. Затем, когда #include его снова, они могут полностью оптимизировать загрузку файла. Тем не менее, ваша рекомендация о предварительных декларациях очень разумна, чтобы уменьшить количество включений. Как только вы начнете использовать предварительные декларации, он станет балансом времени выполнения компилятора (улучшенным за счет предварительных объявлений) и удобством для пользователя (улучшенным за счет удобных дополнительных #includes), что является балансом, который каждая компания устанавливает по-своему.
Корт Аммон - Восстановить Монику

1
@CortAmmon Типичный заголовок содержит элементы защиты, но компилятор все равно должен его открыть, и это медленная операция
BЈови at

4
@ BЈовић: На самом деле они этого не делают. Все, что им нужно сделать, это распознать, что файл имеет «типичные» средства защиты заголовков, и пометить их так, чтобы он открывал его только один раз. Gcc, например, имеет документацию относительно того, когда и где он применяет эту оптимизацию: gcc.gnu.org/onlinedocs/cppinternals/Guard-Macros.html
Cort Ammon - Восстановить Монику

-4

Хорошей практикой является не беспокоиться о вашей стратегии заголовка, пока она компилируется.

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

И да, у вас будут проблемы, связанные с порядком, как только вы начнете попадать на friendземлю.

Вы можете думать о проблеме в двух случаях.


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

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


Случай 2: у вас есть десятки классов. Некоторые из классов представляют основу вашей программы, и перезапись их заголовков заставит вас переписать / перекомпилировать большое количество вашей кодовой базы. Другие классы используют эту основу для выполнения задач. Это представляет собой типичный бизнес-обстановку. Заголовки распределены по каталогам, и вы не можете реально вспомнить имена всех.

Решение: на этом этапе вам нужно думать о своих классах в логических группах и собирать эти группы в заголовки, которые мешают вам #includeснова и снова. Это не только упрощает жизнь, но и является необходимым шагом для использования преимуществ предварительно скомпилированных заголовков .

Вы заканчиваете #includeзанятия, которые вам не нужны, но кого это волнует ?

В этом случае ваш код будет выглядеть так ...

#include <Graphics.hpp>

struct Entity {
    Texture texture;
    RenderObject render();
}

13
Мне пришлось это -1, потому что я искренне верю, что любое предложение в форме «Хорошая практика - не беспокоиться о своей стратегии ____, пока она собирается», приводит людей к неправильному суждению. Я обнаружил, что этот подход очень быстро ведет к нечитаемости, и нечитаемость почти так же плоха, как "не работает". Я также нашел много крупных библиотек, которые не согласны с результатами обоих описанных вами случаев. В качестве примера, Boost выполняет заголовки «коллекций», которые вы рекомендуете в случае 2, но они также имеют большое значение, предоставляя заголовки для каждого класса, когда они вам нужны.
Корт Аммон - Восстановить Монику

3
Я лично был свидетелем того, как «не волнуйтесь, если он компилируется», превращается в «наше приложение компилируется за 30 минут, когда вы добавляете значение в перечисление, как, черт возьми, мы это исправляем !?»
Gort the Robot

Я ответил на вопрос времени компиляции в своем ответе. Фактически, мой ответ - один из двух (ни один из которых не был оценен хорошо), который делает. Но на самом деле это имеет отношение к вопросу ОП; это "Должен ли я в случае верблюда мои имена переменных?" Тип вопроса. Я понимаю, что мой ответ непопулярен, но не всегда лучшая практика для всего, и это один из таких случаев.
QuestionC

Согласитесь с # 2. Что касается более ранних идей - я надеюсь на автоматизацию, которая обновит блок локального заголовка - до тех пор, я выступаю за полный список.
Chux - Восстановить Монику

Подход «включи все и кухонная раковина» может сперва сэкономить вам время - ваши заголовочные файлы могут даже выглядеть меньше (так как большинство вещей включены косвенно откуда-то…). До тех пор, пока вы не дойдете до того момента, когда любое изменение где-либо повлечет за собой 30-минутное повторение вашего проекта. И ваше автозаполнение IDE-smart вызывает сотни неуместных предложений. И вы случайно перепутали два класса с одинаковыми именами или статические функции. И вы добавляете новую структуру, но затем сборка завершается неудачно, потому что у вас есть столкновение пространства имен с совершенно не связанным классом где-то ...
CharonX
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.