Чи належна практика покладатися на те, що заголовки будуть включені транзитивно?


37

Я прибираю включені в проект 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.
sampathsris

@Dunk Я думаю, ви неправильно зрозуміли проблему. З будь-якою з його пропозицій цього не повинно відбутися.
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) повинен бути здатний скласти або проаналізувати самостійно.

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

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

З іншого боку, це не має значення (як правило), якщо ви #include файл, який вам не потрібен ...


В якості особистого стилю я впорядковую #include файли в алфавітному порядку, розбиті на системні та додаткові - це допомагає посилити повідомлення про "самодостатнє та повністю узгоджене".


Примітка щодо порядку включає: іноді важливий порядок, наприклад, коли включаються заголовки X11. Це може бути пов’язано з дизайном (який у такому випадку можна вважати поганим дизайном), іноді це пов'язано з нещасними проблемами несумісності.
Гайд

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

2

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

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

Приклад: Ах включений Bh, який використовується C.cpp. Якщо Bh використовував Ah для деяких деталей реалізації, тоді C.cpp не повинен вважати, що Bh продовжуватиме це робити. Але якщо Bh використовує Ah для базового класу, тоді C.cpp може припустити, що Bh продовжить включати відповідні заголовки для своїх базових класів.

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


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

Я використовую подібний дещо інший підхід від запропонованих відповідей.

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

У вихідних файлах не так важливо, скільки ви включаєте. Мої вподобання все ще включають мінімум, щоб він пройшов.

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

Хоча трохи застарілий, дизайн програмного забезпечення великих масштабів C ++ (Джон Лакос) пояснює все це детально.


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

@Andrew є інструменти та сценарії, щоб перевірити, що і скільки разів включено.
BЈоviћ

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

1
@CortAmmon Типовий заголовок містить охоронці, але компілятор все-таки має його відкрити, і це повільна робота
BЈович

4
@ BЈoviћ: Насправді, вони ні. Все, що вони повинні зробити, це визнати, що файл має "типові" захисні заголовки та позначити їх так, що він відкриває його лише один раз. Наприклад, Gcc
Cort Ammon

-4

Доброю практикою є не турбуватися про стратегію заголовка, поки вона складається.

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

І так, ви будете мати ці проблеми порядку на основі , як тільки ви почнете отримувати в friendземлю.

Про проблему можна думати у двох випадках.


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

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


Випадок 2: У вас десятки класів. Деякі класи представляють основу вашої програми, і перезапис їх заголовків змусить вас переписати / перекомпілювати велику кількість кодової бази. Інші класи використовують цю основу для досягнення справ. Це являє собою типовий бізнес-параметр. Заголовки розкинуті по каталогах, і ви не можете реально запам'ятати назви всього.

Рішення: На цьому етапі вам потрібно подумати про свої заняття в логічних групах і зібрати ті групи в заголовки, які не дозволять вам #include переробляти знову і знову. Це не лише робить життя простішим, але й необхідним кроком до використання попередньо складених заголовків .

Ти закінчишся #include заняття, які вам не потрібні, але кого це хвилює ?

У цьому випадку ваш код виглядатиме як ...

#include <Graphics.hpp>

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

13
Мені довелося це зробити -1, тому що я чесно вірю, що будь-яке речення, яке є у формі "Доброї практики - не турбуватися про свою ____ стратегію, доки вона складається", приводить людей до поганого судження. Я виявив, що підхід дуже швидко призводить до нечитабельності, а нечитабельність - НАЙКРАЩЕ так само погано, як "не працює". Я також знайшов багато основних бібліотек, які не згодні з результатами обох описаних вами випадків. Як приклад, Boost DOE робить заголовки "колекцій", які ви рекомендуєте у випадку 2, але вони також роблять велику справу із надання заголовків класу за класом, коли вони вам потрібні.
Корт Аммон

3
Я особисто був свідком "не хвилюйся, якщо він збирається" перетвориться на "наш додаток займає 30 хвилин, щоб скласти, коли ти додаєш значення перерахункам, як, чорт ми це виправляємо!"
Gort the Robot

Я вирішив питання про час збирання у своїй відповіді. Насправді моя відповідь - це одна з двох (жодна з яких не набрала добре). Але насправді це дотично для питання ОП; це "Чи повинен я верблюд-регістр моїх змінних імен?" питання типу. Я усвідомлюю, що моя відповідь непопулярна, але не завжди є найкраща практика для всього, і це один із таких випадків.
QuestionC

Погодьтеся з №2. Щодо попередніх ідей - я сподіваюся на автоматизацію, яка б оновлювала локальний блок заголовків - до цього часу я виступаю за повний список.
chux

Підхід "включити все і кухонну раковину" може спочатку заощадити певний час - файли заголовків можуть виглядати навіть меншими розмірами (оскільки більшість речей включено побічно з .... десь). Поки ви не прийдете до того моменту, коли будь-яка зміна в будь-якому місці спричиняє повторення 30+ хвилин вашого проекту. І ваш інтелектуальний автозаповнення IDE пропонує сотні невідповідних пропозицій. І ви випадково змішуєте два занадто подібних класи або статичні функції. І ви додаєте нову структуру, але тоді збірка не вдається, оскільки у вас десь зіткнення простору імен з абсолютно непов'язаним класом ...
CharonX
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.