Мы начинаем новый проект с нуля. Около восьми разработчиков, около десятка подсистем, у каждого по четыре или пять исходных файлов.
Что мы можем сделать, чтобы предотвратить «адский заголовок», ака «заголовки спагетти»?
- Один заголовок на исходный файл?
- Плюс один на подсистему?
- Отделить typdefs, stucts & enums от прототипов функций?
- Отделить внутреннюю подсистему от внешней подсистемы?
- Настаивать на том, что каждый отдельный файл, будь то заголовок или источник, должен быть автономно компилируемым?
Я не спрашиваю о «лучшем» способе, просто указываю на то, на что следует обращать внимание и что может вызвать горе, чтобы мы могли попытаться избежать этого.
Это будет проект C ++, но информация C поможет будущим читателям.
Ответы:
Простой метод: один заголовок на исходный файл. Если у вас есть полная подсистема, в которой пользователи не должны знать об исходных файлах, имейте один заголовок для подсистемы, включая все необходимые файлы заголовков.
Любой заголовочный файл должен быть компилируемым сам по себе (или, скажем, исходный файл, включающий любой отдельный заголовок, должен компилироваться). Будет больно, если я обнаружу, какой заголовочный файл содержит то, что я хочу, и тогда мне придется искать другие заголовочные файлы. Простой способ добиться этого состоит в том, чтобы каждый исходный файл сначала включал заголовочный файл (спасибо, doug65536, я думаю, что я делаю это большую часть времени, даже не осознавая этого).
Убедитесь, что вы используете доступные инструменты, чтобы сократить время компиляции - каждый заголовок должен быть включен только один раз, используйте предварительно скомпилированные заголовки, чтобы сократить время компиляции, используйте предварительно скомпилированные модули, если это возможно, чтобы сократить время компиляции.
источник
Безусловно, наиболее важным требованием является уменьшение зависимости между вашими исходными файлами. В C ++ обычно используется один исходный файл и один заголовок на класс. Поэтому, если у вас хороший дизайн класса, вы даже близко не приблизитесь к адскому заголовку.
Вы также можете посмотреть на это с другой стороны: если у вас уже есть адский заголовок в вашем проекте, вы можете быть совершенно уверены, что дизайн программного обеспечения нуждается в улучшении.
Чтобы ответить на ваши конкретные вопросы:
источник
В дополнение к другим рекомендациям, в соответствии с сокращением зависимостей (в основном применимо к C ++):
источник
Один заголовок на исходный файл, который определяет, что его исходный файл реализует / экспортирует.
Столько заголовочных файлов, сколько необходимо, включенных в каждый исходный файл (начиная с собственного заголовка).
Избегайте включения (минимизируйте включение) заголовочных файлов в другие заголовочные файлы (чтобы избежать циклических зависимостей). Подробнее см. Ответ на вопрос «могут ли два класса видеть друг друга с помощью C ++?»
На эту тему есть целая книга Lakos, посвященная крупномасштабному программному обеспечению для C ++ . Он описывает наличие «слоев» программного обеспечения: высокоуровневые слои используют низкоуровневые слои, а не наоборот, что опять-таки позволяет избежать циклических зависимостей.
источник
Я бы сказал, что ваш вопрос принципиально не отвечает, поскольку существует два вида адских заголовков:
Дело в том, что если вы попытаетесь избежать первого, то в какой-то степени вы получите второе, и наоборот.
Существует также третий вид ада, это круговые зависимости. Они могут появиться, если вы не будете осторожны ... избегать их не сложно, но вам нужно время, чтобы подумать, как это сделать. См Джон LAKOS говорить о Levelization в CppCon 2016 году (или только слайды ).
источник
Развязка
В конечном счете, речь идет о развязке для меня на самом фундаментальном уровне проектирования, лишенном нюансов характеристик наших компиляторов и компоновщиков. Я имею в виду, что вы можете делать такие вещи, как сделать так, чтобы каждый заголовок определял только один класс, использовать pimpls, пересылать объявления типам, которые нужно только объявить, но не определить, возможно, даже использовать заголовки, которые просто содержат прямые объявления (например:)
<iosfwd>
, один заголовок на исходный файл организовать систему последовательно, основываясь на типе декларируемой / определенной вещи и т. д.Методы уменьшения "зависимостей времени компиляции"
И некоторые из методов могут немного помочь, но вы можете исчерпать эти методы и все же найти свой средний исходный файл в вашей системе, нуждающийся в двухстраничной преамбуле
#include
указание делать что-то немного значимое со стремительно растущим временем сборки, если вы слишком много внимания уделяете сокращению зависимостей во время компиляции на уровне заголовка без уменьшения логических зависимостей в ваших интерфейсах, и хотя это, строго говоря, не может считаться «заголовками спагетти», я Я бы все еще сказал, что это приводит к подобным вредным проблемам производительности на практике. В конце концов, если ваши модули компиляции по-прежнему требуют, чтобы для выполнения чего-либо было достаточно информации, то это приведет к увеличению времени сборки и умножению причин, по которым вам придется вернуться назад и что-то менять, в то время как разработчики делают чувствую, что они бьют головой систему, просто пытаясь закончить свое ежедневное кодирование. Это'Например, вы можете заставить каждую подсистему предоставлять один очень абстрактный заголовочный файл и интерфейс. Но если подсистемы не отделены друг от друга, то вы снова получаете нечто похожее на спагетти с интерфейсами подсистем, зависящими от других интерфейсов подсистем с графом зависимостей, который для работы выглядит как беспорядок.
Пересылка объявлений внешним типам
Из всех методов, которые я исчерпал, чтобы попытаться получить прежнюю кодовую базу, на создание которой ушло два часа, в то время как разработчики иногда ждали своей очереди в CI на наших серверах сборки (2 дня) (вы можете почти представить, что эти машины сборки истощены чудовищными усилиями, отчаянно пытаясь чтобы не отставать и терпеть неудачу, пока разработчики продвигают свои изменения), самым сомнительным для меня было прямое объявление типов, определенных в других заголовках. И мне удалось довести эту кодовую базу до 40 минут или около того после целого ряда этапов, которые делали это небольшими пошаговыми шагами, пытаясь уменьшить «заголовок спагетти», наиболее сомнительную практику в ретроспективе (например, заставляя меня упускать из виду фундаментальную природу проектирование, в то время как туннель рассматривал взаимозависимости заголовков), было прямым объявлением типов, определенных в других заголовках.
Если вы представляете
Foo.hpp
заголовок, который имеет что-то вроде:И он использует только
Bar
в заголовке способ, который требует объявления, а не определения. тогда это может показаться легким делом, чтобы объявить,class Bar;
чтобы избежать определенияBar
видимого в заголовке. За исключением случаев, когда на практике часто вы либо обнаружите, что большинство используемых модулей компиляцииFoo.hpp
все равно в конечном итоге нуждаютсяBar
в определении с дополнительным бременем необходимости включатьBar.hpp
себя поверхFoo.hpp
, либо вы столкнетесь с другим сценарием, где это действительно помогает и % ваших модулей компиляции могут работать без включенияBar.hpp
, за исключением того, что это поднимает более фундаментальный вопрос проектирования (или, по крайней мере, я думаю, что это должно происходить в настоящее время), почему они должны даже видеть объявлениеBar
и почемуFoo
даже нужно беспокоиться, чтобы узнать об этом, если это не имеет отношения к большинству случаев использования (зачем обременять дизайн зависимостями с другим, который едва когда-либо использовался?).Потому что концептуально мы действительно не отделены
Foo
отBar
. Мы только что сделали это, чтобы заголовокFoo
не нуждался в таком большом количестве информации о заголовкеBar
, и это не так существенно, как дизайн, который действительно делает эти два полностью независимыми друг от друга.Встроенные сценарии
Это действительно для крупномасштабных кодовых баз, но другой метод, который я считаю чрезвычайно полезным, - это использование встроенного языка сценариев, по крайней мере, для самых высокоуровневых частей вашей системы. Я обнаружил, что смог встраивать Lua за один день и иметь возможность одинаково вызывать все команды в нашей системе (к счастью, команды были абстрактными). К сожалению, я наткнулся на контрольно-пропускной пункт, где разработчики не доверяли внедрению другого языка и, возможно, наиболее странно, с производительностью как их наибольшим подозрением. Тем не менее, хотя я мог бы понять и другие проблемы, производительность не должна быть проблемой, если мы используем сценарий только для вызова команд, когда пользователи нажимают кнопки, например, которые не выполняют своих собственных значительных циклов (что мы пытаемся сделать, беспокоиться о разнице наносекунд во времени отклика на нажатие кнопки?).
пример
Между тем, наиболее эффективным способом, который я когда-либо видел после исчерпания методов сокращения времени компиляции в больших кодовых базах, являются архитектуры, которые действительно уменьшают объем информации, необходимой для работы какой-либо одной вещи в системе, а не просто отсоединяют один заголовок от другого от компилятора. в перспективе, но требуя, чтобы пользователи этих интерфейсов делали то, что им нужно делать, зная (как с точки зрения человека, так и с точки зрения компилятора, истинная развязка, выходящая за пределы зависимостей компилятора), как минимум.
ECS - это всего лишь один пример (и я не предлагаю вам его использовать), но столкнувшись с ним, я понял, что у вас могут быть действительно эпичные кодовые базы, которые все еще создаются на удивление быстро, при этом успешно используя шаблоны и множество других полезностей, потому что ECS, благодаря природа создает очень несвязную архитектуру, в которой системам нужно знать только о базе данных ECS и, как правило, лишь несколько типов компонентов (иногда только один), чтобы выполнять свою задачу:
Дизайн, Дизайн, Дизайн
И такого рода разъединенные архитектурные проекты на человеческом, концептуальном уровне более эффективны с точки зрения минимизации времени компиляции, чем любой из методов, которые я исследовал выше, по мере того, как ваша кодовая база растет, растет и растет, потому что этот рост не переводится на ваш средний уровень. модуль компиляции, умножающий количество информации, необходимой при компиляции, и время компоновки для работы (любая система, которая требует, чтобы ваш средний разработчик включил множество вещей, чтобы сделать что-либо, также требует их, а не только компилятор должен знать о большом количестве информации, чтобы сделать что-нибудь ). Он также имеет больше преимуществ, чем сокращение времени сборки и распутывание заголовков, поскольку это также означает, что разработчикам не нужно много знать о системе, помимо того, что требуется немедленно, чтобы что-то с ней сделать.
Если, например, вы можете нанять опытного разработчика физики для разработки физического движка для вашей игры AAA, который охватывает миллионы LOC, и он может начать работу очень быстро, зная абсолютную минимальную информацию о таких вещах, как типы и доступные интерфейсы а также ваши системные понятия, то это, естественно, приведет к уменьшению объема информации, необходимой и ему, и компилятору для создания его физического движка, и аналогичным образом приведет к значительному сокращению времени сборки, в то время как обычно подразумевается, что нет ничего похожего на спагетти. где-нибудь в системе. И вот что я предлагаю расставить приоритеты над всеми этими другими методами: как вы проектируете свои системы. Исчерпание других техник будет обледенением, если вы сделаете это в то время, в противном случае,
источник
Это вопрос мнения. Смотрите этот ответ и этот . И это также сильно зависит от размера проекта (если вы считаете, что в вашем проекте будут миллионы строк исходного текста, это не то же самое, что иметь несколько десятков тысяч).
В отличие от других ответов, я рекомендую один (довольно большой) публичный заголовок для каждой подсистемы (который может включать «частные» заголовки, возможно, имеющие отдельные файлы для реализации многих встроенных функций). Вы могли бы даже рассмотреть заголовок, имеющий только несколько
#include
директив.Я не думаю, что много заголовочных файлов рекомендуется. В частности, я не рекомендую один заголовочный файл на класс или много маленьких заголовочных файлов по несколько десятков строк в каждом.
(Если у вас много маленьких файлов, вам нужно будет включить их в каждую маленькую единицу перевода , и общее время сборки может пострадать)
Что вы действительно хотите, так это определить для каждой подсистемы и файла главного разработчика, ответственного за это.
Наконец, для небольшого проекта (например, менее ста тысяч строк исходного кода) это не очень важно. Во время проекта вы легко сможете реорганизовать код и реорганизовать его в разные файлы. Вы просто скопируете и вставите куски кода в новые (заголовочные) файлы, что не составляет особого труда (что сложнее - продумать, как вы будете реорганизовывать свои файлы, и это зависит от проекта).
(лично я предпочитаю избегать слишком больших и слишком маленьких файлов; у меня часто исходные файлы по несколько тысяч строк в каждом; и я не боюсь заголовочного файла, включая определения встроенных функций, из нескольких сотен строк или даже пары тысячи их)
Обратите внимание, что если вы хотите использовать предварительно скомпилированные заголовки с GCC (что иногда является разумным подходом для сокращения времени компиляции), вам нужен один файл заголовка (включая все остальные, а также системные заголовки).
Обратите внимание, что в C ++ стандартные заголовочные файлы требуют много кода . Например
#include <vector>
, тянет более десяти тысяч строк на моем GCC 6 в Linux (18100 строк). И#include <map>
расширяется почти до 40KLOC. Следовательно, если у вас есть много маленьких заголовочных файлов, включая стандартные заголовки, вы в конечном итоге анализируете много тысяч строк во время сборки, и ваше время компиляции страдает. Вот почему мне не нравится иметь много маленьких строк исходного кода C ++ (максимум несколько сотен строк), но я предпочитаю иметь меньше, но больше файлов C ++ (из нескольких тысяч строк).(поэтому наличие сотен маленьких файлов C ++, которые всегда включают - даже косвенно - несколько стандартных заголовочных файлов, дает огромное время сборки, что раздражает разработчиков)
В C-коде довольно часто файлы заголовков расширяются до чего-то меньшего, поэтому компромисс будет другим.
Также обратите внимание на предварительную практику в существующих проектах свободного программного обеспечения (например, на github ).
Обратите внимание, что с зависимостями можно справиться с хорошей системой автоматизации сборки . Изучите документацию по GNU make . Помните о различных
-M
флагах препроцессора для GCC (полезно для автоматической генерации зависимостей).Другими словами, ваш проект (с менее чем сотней файлов и дюжиной разработчиков), вероятно, недостаточно велик, чтобы действительно беспокоиться о «заголовке ада», поэтому ваше беспокойство не оправдано . Вы можете иметь только дюжину заголовочных файлов (или даже намного меньше), вы можете выбрать один заголовочный файл на единицу перевода, вы даже можете выбрать один единственный заголовочный файл, и все, что вы выберете, не будет «заголовок ада» (а рефакторинг и реорганизация ваших файлов останутся достаточно простыми, поэтому первоначальный выбор не очень важен ).
(Не сосредотачивайте свои усилия на «адском заголовке» - это не проблема для вас, но сосредоточьтесь на них, чтобы спроектировать хорошую архитектуру)
источник