Як я можу запобігти пекла заголовка?


45

Ми починаємо новий проект, з нуля. Близько восьми розробників, близько десятка підсистем, кожен з чотирма або п’ятьма вихідними файлами.

Що ми можемо зробити, щоб не допустити "пекло заголовка", AKA "заголовки спагетті"?

  • Один заголовок на вихідний файл?
  • Плюс один на підсистему?
  • Відокремити typedefs, stuct & enums від прототипів функцій?
  • Відокремлена підсистема внутрішня від зовнішньої частини підсистеми?
  • Наполягайте на тому, що кожен окремий файл, будь-який заголовок чи джерело, має бути окремим для компіляції?

Я не прошу "найкращого" шляху, просто вкажіть, на що слідкувати і що може спричинити горе, щоб ми могли спробувати уникнути цього.

Це буде проект C ++, але інформація про C допоможе майбутнім читачам.


16
Отримайте копію великомасштабного програмного забезпечення C ++ , це не просто навчить уникати проблем із заголовками, але й багато інших проблем, що стосуються фізичних залежностей між файлами джерела та об’єктів у проекті C ++.
Док Браун

6
Усі відповіді тут чудові. Я хотів додати, що документація щодо використання об’єктів, методів, функцій повинна бути у файлах заголовка. Я все ще бачу документ у вихідних файлах. Не змушуйте мене читати джерело. Це сенс файлу заголовка. Мені не потрібно читати джерело, якщо я не виконавець.
Білл-двері

1
Я впевнений, що раніше працював з вами. Часто :-(
Mawg

5
Те, що ви описуєте, не є великим проектом. Хороший дизайн завжди вітається, але ви, можливо, ніколи не зіткнетеся з проблемами "Системи великих масштабів".
Сам

2
Підсилення насправді має всеохоплюючий підхід. Кожна окрема функція має власний файл заголовка, але кожен більший модуль також має заголовок, який включає все. Це виявляється по-справжньому потужним для мінімізації пекла заголовка, не змушуючи вас #include декілька сотень файлів кожного разу.
Корт Аммон

Відповіді:


39

Простий метод: один заголовок на вихідний файл. Якщо у вас є повна підсистема, де користувачі не будуть знати про вихідні файли, є один заголовок для підсистеми, що включає всі необхідні файли заголовків.

Будь-який файл заголовка повинен складатись самостійно (або скажімо, вихідний файл, включаючи будь-який окремий заголовок). Біль, якщо я знайшов, який файл заголовка містить те, що я хочу, і тоді мені доведеться шукати інші файли заголовків. Простий спосіб застосувати це, щоб кожен вихідний файл включив спочатку його файл заголовка (спасибі doug65536, я думаю, що я роблю це більшу частину часу, навіть не усвідомлюючи).

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


Там, де стає хитромудро, є перехресні підсистеми функцій, з параметрами типів, оголошених в іншій підсистемі.
Мавг

6
Хитрі чи ні, "#include <subystem1.h>" повинен компілюватись. Як ви цього досягнете, залежить від вас. @FrankPuffer: Чому?
gnasher729

13
@Mawg Це вказує, що вам потрібна окрема спільна підсистема, що включає спільність окремих підсистем, або вам потрібні спрощені заголовки "інтерфейсу" для кожної підсистеми (яка потім використовується заголовками реалізації, як внутрішніми, так і міжсистемними) . Якщо ви не можете написати заголовки інтерфейсу без перехресних включень, дизайн вашої підсистеми переплутався, і вам потрібно переробити речі, щоб ваші підсистеми були більш незалежними. (Що може включати витяг загальної підсистеми як третій модуль.)
RM

8
Хорошою технікою забезпечення заголовка є незалежним - це правило, згідно з яким вихідний файл завжди містить спочатку власний заголовок . Це дозволить зафіксувати випадки, коли потрібно перемістити залежність, що не входить у файл реалізації, у файл заголовка.
doug65536

4
@FrankPuffer: Будь ласка, не видаляйте ваші коментарі, особливо якщо на них реагують інші, оскільки це робить відповіді безпредметними. Ви завжди можете виправити свою заяву в новому коментарі. Дякую! Мені цікаво дізнатись, що ви насправді сказали, але зараз його вже немає :(
MPW

18

На сьогодні найголовніша вимога - зменшити залежності між вихідними файлами. У C ++ прийнято використовувати один вихідний файл і один заголовок на клас. Тому, якщо у вас хороший дизайн класу, ви навіть не наблизитесь до пекла заголовка.

Ви також можете переглянути це навпаки: якщо у вашому проекті вже є пекло заголовка, ви можете бути впевнені, що дизайн програмного забезпечення потрібно вдосконалити.

Щоб відповісти на ваші конкретні запитання:

  • Один заголовок на вихідний файл? → Так, у більшості випадків це добре працює і полегшує пошук матеріалів. Але не робіть це релігією.
  • Плюс один на підсистему? → Ні, чому б ви хотіли це робити?
  • Відокремити typedefs, stuct & enums від прототипів функцій? → Ні, функції та споріднені типи належать разом.
  • Відокремлена підсистема внутрішня від зовнішньої частини підсистеми? → Так, звичайно. Це зменшить залежності.
  • Наполягайте на тому, щоб кожен файл, будь то заголовок чи джерело в автономному режимі, сумісний? → Так, ніколи не вимагайте, щоб будь-який заголовок був включений перед іншим заголовком.

12

На додаток до інших рекомендацій, відповідно до зменшення залежностей (в основному застосовних до C ++):

  1. Включіть лише те, що вам дійсно потрібно, де вам це потрібно (найнижчий рівень можливий). E. g. не включайте в заголовок, якщо вам потрібні дзвінки лише в джерелі.
  2. Коли можливо, використовуйте прямі декларації в заголовках (заголовок містить лише вказівники або посилання на інші класи).
  3. Очистіть компоненти після кожного рефакторингу (прокоментуйте їх, подивіться, де компіляція провалюється, перемістіть їх туди, видаліть все ще коментовані рядки включення).
  4. Не упакуйте занадто багато загальних засобів в один файл; розділити їх за функціональністю (наприклад, Logger - один клас, таким чином, один заголовок і один вихідний файл; SystemHelper dito. тощо).
  5. Дотримуйтесь принципів ОО, навіть якщо все, що ви отримуєте, - це клас, що складається виключно з статичних методів (замість самостійних функцій) - або використовуйте натомість простір імен .
  6. Для певних загальних об'єктів однотонний шаблон є досить корисним, оскільки вам не потрібно запитувати екземпляр від якогось іншого, не пов'язаного між собою об'єкта.

5
У №3 інструмент включає в себе те, що ви використовуєте, може допомогти, уникаючи підходу до вгадування та перевірки ручної рекомпіляції.
РМ

1
Чи можете ви пояснити, яка користь одинаків у цьому контексті? Я справді цього не розумію.
Френк Пуффер

@FrankPuffer Моє обгрунтування таке: Без одинакового певного екземпляра класу зазвичай належить екземпляр класу помічників, як Logger. Якщо третій клас хоче використовувати його, йому потрібно запитати посилання на клас помічника у власника, це означає, що ви використовуєте два класи, і, звичайно, включати їх заголовки - навіть якщо користувач не має жодної справи з власником. За допомогою одинарного вам потрібно лише включити заголовок helper-класу і можете запитувати екземпляр безпосередньо з нього. Ви бачите недолік у цій логіці?
Мерфі

1
№2 (прямі декларації) можуть змінити величину часу у складанні та перевірці залежності. Як показує ця відповідь ( stackoverflow.com/a/9999752/509928 ), вона стосується як C ++, так і C
Дейва Комптона

3
Ви також можете використовувати декларації вперед при проходженні за значенням, доки функція не буде визначена вбудованою. Оголошення функції не є контекстом, де потрібне повне визначення типу (визначення функції або виклик функції є таким контекстом).
StoryTeller - Невідповідна Моніка

6

Один заголовок на вихідний файл, який визначає, що його вихідний файл реалізує / експортує.

Скільки файлів заголовків, скільки потрібно, включено до кожного вихідного файлу (починаючи з власного заголовка).

Уникайте включення (мінімізуйте включення) файлів заголовків до інших файлів заголовків (щоб уникнути кругових залежностей). Детальніше див. Цю відповідь на те, "чи можуть два класи бачити один одного за допомогою C ++?"

На цю тему існує ціла книга , широкомасштабний дизайн програмного забезпечення C ++ від Lakos. Він описує наявність "шарів" програмного забезпечення: шари високого рівня використовують шари нижчого рівня, а не навпаки, що знову дозволяє уникнути кругових залежностей.


4

Я б сказав, що ваше питання принципово невідповідне, оскільки існує два види пекла заголовка:

  • Такого роду, де потрібно включити мільйон різних заголовків, а хто в пеклі може навіть згадати їх усіх? І підтримувати ці списки заголовків? Тьфу.
  • Вигляд, де ви включаєте одне, і дізнаєтесь, що ви включили всю Вавилонську вежу (чи я повинен сказати башту підсилення? ...)

річ у тому, що якщо ви намагаєтесь уникнути першого, ви в деякій мірі закінчуєтесь з останнім, і навпаки.

Є й третій вид пекла, який є круговими залежностями. Вони можуть з’явитися, якщо ви не обережні ... уникати їх не надто складно, але вам потрібно витратити час, щоб подумати над тим, як це зробити. Дивіться розмову Джона Лакоса про Levelization в CppCon 2016 (або лише слайдах ).


1
Не завжди можна уникнути кругових залежностей. Прикладом може слугувати модель, в якій суб’єкти посилаються один на одного. Принаймні, ви можете спробувати обмежити циркулярність в підсистемі, це означає, що якщо ви включите заголовок підсистеми, ви абстрагувались циркулярності.
напів

2
@nalply: Я мав на увазі уникнути кругової залежності заголовків, а не коду ... якщо ви не уникнете залежності кругового заголовка, ви, ймовірно, не зможете побудувати. Але так, взятий пункт, +1.
einpoklum - відновити Моніку

1

Розв'язка

Це в кінцевому рахунку про розв’язку для мене в кінці дня на самому фундаментальному рівні дизайну, позбавленому нюансу характеристик наших компіляторів і лінкерів. Я маю на увазі, що ви можете робити такі речі, як зробити так, щоб кожен заголовок визначав лише один клас, використовувати pimpls, пересилати декларації на типи, які потрібно лише оголосити, не визначити, можливо, навіть використовувати заголовки, які містять лише прямі декларації (наприклад:) <iosfwd>, один заголовок на вихідний файл , систематизувати систему послідовно на основі типу речі, яка оголошується / визначається тощо.

Методи зменшення "залежностей від компіляції від часу"

І деякі з цих методів можуть допомогти трохи, але ви можете вичерпати ці практики та все ж знайти свій середній вихідний файл у вашій системі потребує двосторінкової преамбули #includeдирективи робити що-небудь трохи осмислене в часи збірки з високою швидкістю, якщо ви зосереджуєтесь на тому, щоб зменшити залежність часу компіляції на рівні заголовка, не зменшуючи логічних залежностей в дизайні інтерфейсу, і хоча це не може вважатися "заголовками спагетті" строго кажучи, я Ще сказати, це означає подібні згубні питання, як продуктивність на практиці. Зрештою, якщо вашим підрозділам компіляції все-таки потрібен набір інформації, щоб було видно, щоб зробити що-небудь, то це буде перекладено на збільшення часу збірки та примноження причин, по яких ви потенційно можете повернутися назад і змінити речі, роблячи розробників. відчуваю, що вони головують над системою, просто намагаються завершити щоденне кодування. Це '

Наприклад, ви можете змусити кожну підсистему надати один дуже абстрактний файл заголовка та інтерфейс. Але якщо підсистеми не відокремлюються одна від одної, то ви отримуєте щось, що нагадує спагетті знову з інтерфейсами підсистеми залежно від інших інтерфейсів підсистеми з графіком залежності, який схожий на безлад для того, щоб працювати.

Передача декларацій до зовнішніх типів

З усіх методів я виснажився, щоб спробувати отримати колишню базу коду, яка займала дві години, а розробники інколи чекали 2 дні на свою чергу в CI на наших серверах побудови (ви майже можете уявити, що ці верстати будують як виснажені звірі з тягаря, несамовито намагаючись щоб не відставати, поки розробники не змінюють свої зміни), для мене найбільш сумнівними були типи оголошення вперед, визначені в інших заголовках. І мені вдалося зменшити цю кодову базу до 40 хвилин або близько того після віку, роблячи це невеликими поступовими кроками, намагаючись зменшити "заголовки спагетті", найбільш сумнівної практики заднього огляду (як змусити мене втратити з уваги фундаментальний характер конструкція, коли тунель бачив взаємозалежності заголовків), оголошував типи вперед, визначені в інших заголовках.

Якщо ви уявляєте Foo.hppзаголовок, який має щось на кшталт:

#include "Bar.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, і він може почати дуже швидко, знаючи абсолютний мінімум інформації, що стосується речей, таких як типи та інтерфейси. а також ваші системні концепції, то це, природно, буде перекладено на зменшену кількість інформації як для нього, так і для компілятора, щоб вимагати побудови свого фізичного двигуна, а також перекладати до значного скорочення часу складання, загалом маючи на увазі, що немає нічого подібного до спагетті в будь-якій точці системи. І саме це я пропоную розставити пріоритет над усіма цими іншими методами: як ви проектуєте свої системи. Вичерпання інших прийомів буде обмерзанням зверху, якщо ви це зробите, інакше,


1
Відмінна відповідь! Althoguh Мені довелося трохи копати, щоб дізнатись, що таке сутенери :-)
Mawg

0

Це питання думки. Дивіться цю відповідь і цю . І це також багато що залежить від розміру проекту (якщо ви вважаєте, що у вашому проекті буде мільйони ліній джерел, це не те саме, що їх мати кілька десятків тисяч).

На відміну від інших відповідей, я рекомендую один (досить великий) загальнодоступний заголовок на підсистему (який може включати "приватні" заголовки, можливо, мають окремі файли для реалізації багатьох вбудованих функцій). Можна навіть розглянути заголовок, що має лише кілька #include директив.

Я не думаю, що рекомендується багато файлів заголовків. Зокрема, я не рекомендую один файл заголовка в класі або багато невеликих файлів заголовків по кілька десятків рядків кожен.

(Якщо у вас є велика кількість невеликих файлів, вам потрібно буде включити їх багато в кожну невелику одиницю перекладу , і загальний час збірки може постраждати)

Насправді ви хочете визначити для кожної підсистеми та файлу головного розробника, відповідального за неї.

Нарешті, для невеликого проекту (наприклад, менше ста тисяч рядків вихідного коду) це не дуже важливо. Під час проекту ви будете досить легко переробляти код і реорганізувати його в різні файли. Ви просто копіюєте та вставляєте шматки коду в нові файли (заголовок), що не є великою справою (що складніше - розумно спроектувати, як ви б реорганізували свої файли, і це конкретно для проекту).

(мої особисті переваги - уникати занадто великих і занадто малих файлів; у мене часто є вихідні файли по кілька тисяч рядків кожен; і я не боюся файлу заголовка - включаючи вкладені визначення функцій - з багатьох сотень рядків або навіть пари їх тисячі)

Зауважте, що якщо ви хочете використовувати заздалегідь складені заголовки з GCC (що іноді є розумним підходом до скорочення часу компіляції), вам потрібен один файл заголовка (включаючи всі інші, а також заголовки системи).

Зауважте, що в C ++ стандартні файли заголовків тягнуть багато коду . Наприклад #include <vector>, витягую понад десять тисяч рядків на моєму GCC 6 на Linux (18100 рядків). І #include <map> розширюється майже до 40KLOC. Отже, якщо у вас є багато невеликих файлів заголовків, включаючи стандартні заголовки, ви закінчите повторний аналіз багатьох тисяч рядків під час збирання, і ваш час компіляції страждає. Ось чому мені не подобається мати багато невеликих ліній джерел C ++ (максимум кілька сотень рядків), але я вважаю за краще мати менше, але більших файлів C ++ (на кілька тисяч рядків).

(тому наявність сотень невеликих файлів на C ++, які завжди включають-навіть побічно - кілька стандартних файлів заголовків дає величезний час збірки, що дратує розробників)

У коді С досить часто файли заголовків розширюються на щось менше, тому компроміс відрізняється.

Також для натхнення погляньте на попередню практику в існуючих проектах вільного програмного забезпечення (наприклад, на github ).

Зауважте, що залежності можуть бути вирішені хорошою системою автоматизації побудови . Вивчіть документацію GNU make . Будьте в курсі різних -Mпрапорців препроцесора до GCC (корисно автоматично створювати залежності).

Іншими словами, ваш проект (що має менше ста файлів і десяток розробників), мабуть, недостатньо великий, щоб по-справжньому стурбований «пекельним заголовком», тому ваше занепокоєння не виправдане . У вас може бути лише десяток файлів заголовків (або навіть набагато менше), ви можете вибрати один файл заголовка на одиницю перекладу, ви можете навіть один файл заголовка, і все, що ви вирішите зробити, не буде «заголовок пекло» (і рефакторінга і реорганізувати ваші файли будуть залишатися досить легко, тому початковий вибір НЕ дуже важливо ).

(Не зосереджуйте свої зусилля на "пеклі заголовка", що для вас не є проблемою, але зосередьте їх на розробці гарної архітектури)


Згадані вами технічні характеристики можуть бути правильними. Однак, як я це зрозумів, ОП просила підказки, як поліпшити кодова підтримка та організацію, а не час компіляції. І я бачу прямий конфлікт між цими двома цілями.
Мерфі

Але це все-таки питання думки. І ОП, мабуть, починає не настільки великий проект.
Базиль Старинкевич
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.