Кілька класів у файлі заголовка порівняно з одним файлом заголовка на клас


75

З якоїсь причини наша компанія має керівні принципи кодування, в яких зазначено:

Each class shall have it's own header and implementation file.

Отже, якби ми написали клас із назвою, MyStringнам потрібні були б пов’язані MyStringh.h та MyString.cxx .

Хтось ще робить це? Хто-небудь бачив якісь наслідки продуктивності компіляції? Чи складає 5000 класів у 10000 файлах так само швидко, як 5000 класів у 2500 файлах? Якщо ні, то помітна різниця?

[Ми кодуємо C ++ і використовуємо GCC 3.4.4 як наш щоденний компілятор]


74
Рекомендації вашої компанії містять помилку.
Гонки легкості на орбіті

2
До речі, це нечітко. Яка дурна, пухнаста вимога.
Гонки легкості на орбіті

8
Помилка - бездомний апостроф. (присвійне "це" - це "своє") ("це" - це скорочення "воно є").
ctrl-alt-delor

9
В який файл я помістив клас MyString? Я впевнений, що залишив це десь тут. Чи є спосіб полегшити запам’ятовування? Я знаю, що якщо ми ...
ctrl-alt-delor

5
Я бачив код, який має декілька визначень класів в одному файлі заголовка, а файл cpp визначає функції-члени декількох класів. Кошмар налагодження
UmNyobe

Відповіді:


81

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

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

Крім того, ви знайдете багато помилок / діагностика повідомляється через ім'я файлу ("Помилка в Myclass.cpp, рядок 22"), і це допомагає, якщо між файлами та класами існує одна до одної відповідність. (Або я припускаю, що ви можете назвати це кореспонденцією 2 до 1).


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

68

Перевантажені тисячами рядків коду?

Наявність одного набору заголовків / вихідних файлів на клас у каталозі може здатися надмірним. І якщо кількість занять сягає 100 або 1000, це може навіть лякати.

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

Приклад із реального життя: ієрархія класів, визначена в цих тисячах джерел, закрилася у спадщину алмазів, а деякі методи були замінені в дочірніх класах методами з точно однаковим кодом. Це легко було пропустити (хто хоче дослідити / перевірити 20 000 рядків вихідного коду?), І коли початковий метод був змінений (виправлення помилок), ефект був не таким універсальним, як виняток.

Залежності стають круговими?

У мене була проблема з шаблонним кодом, але я бачив подібні проблеми зі звичайним кодом С ++ та С.

Розбиття джерел на 1 заголовок на структуру / клас дозволяє:

  • Прискоріть компіляцію, оскільки ви можете використовувати переадресацію символів замість включення цілих об'єктів
  • Мати кругові залежності між класами (§) (тобто клас A має вказівник на B, а B має вказівник на A)

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

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

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

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

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

(§) Зверніть увагу, що ви можете мати кругові залежності між класами, якщо знаєте, який клас кому належить. Це дискусія щодо класів, які знають про існування інших класів, а не про кругові залежності anti_ Pattern.

Останнє слово: заголовки повинні бути самодостатніми

Однак одне, що повинно дотримуватися рішення кількох заголовків та кількох джерел.

Коли ви включаєте один заголовок, незалежно від того, який заголовок, ваше джерело має компілювати чисто.

Кожен заголовок повинен бути самодостатнім. Ви повинні розробити код, а не шукати скарби, зігріваючи свій проект із понад 10 000 вихідних файлів, щоб знайти, який заголовок визначає символ у заголовку з 1000 рядків, який потрібно включити лише через одне перерахування.

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

Питання про кругові залежності

підкреслення-d запитує:

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

underscore_d, 13 листопада о 23:20

Скажімо, у вас є 2 шаблони класів, A та B.

Скажімо, визначення класу A (відповідно B) має вказівник на B (відповідно A). Скажімо також, що методи класу A (відповідно B) насправді викликають методи з B (відповідно A).

Ви маєте кругову залежність як у визначенні класів, так і в реалізації їх методів.

Якби A і B були нормальними класами, а методи A і B знаходились у файлах .CPP, не виникало б жодних проблем: ви б використовували переадресоване оголошення, мали заголовок для кожного визначення класу, тоді кожен CPP містив би обидва HPP.

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

Це означає:

  1. заголовки визначення A.def.hpp та B.def.hpp
  2. заголовок реалізації A.inl.hpp та B.inl.hpp
  3. для зручності "наївний" заголовок A.hpp та B.hpp

Кожен заголовок матиме такі риси:

  1. В A.def.hpp (відповідно B.def.hpp) у вас є переадресація класу B (відповідно A), що дозволить вам оголосити вказівник / посилання на цей клас
  2. A.inl.hpp (відповідно B.inl.hpp) включатиме як A.def.hpp, так і B.def.hpp, що дозволить методам з A (відповідно B) використовувати клас B (відповідно A) .
  3. A.hpp (відповідно B.hpp) безпосередньо включатиме як A.def.hpp, так і A.inl.hpp (відповідно B.def.hpp і B.inl.hpp)
  4. Звичайно, всі заголовки повинні бути самодостатніми та захищені захисними колонтитулами

Наївний користувач включатиме A.hpp та / або B.hpp, ігноруючи таким чином весь безлад.

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

Зверніть увагу, що це був крайній випадок (два шаблони, що знають один одного). Я сподіваюся, що більшість кодів не потребують цього фокусу.


Чи можете ви пояснити, як використання окремих заголовків має якесь значення для кругових залежностей? Я не думаю, що це так. Ми можемо тривіально створити циклічну залежність, навіть якщо обидва класи повністю оголошені в одному заголовку, просто попередньо оголосивши один заздалегідь, перш ніж оголосити дескриптор для нього в іншому. Все інше здається чудовими моментами, але думка, що окремі заголовки полегшують кругові залежності, здається далекою.
underscore_d

@underscore_d: Я відповів на ваше запитання у своїй відповіді. Скажіть, будь ласка, якщо я щось пропустив.
paercebal

1
Дякую, це все охоплює! Насправді, незручно, читаючи далі, я зрозумів, що раніше стикався з цією проблемою з класами шаблонів і вирішував її в основному так само, за допомогою .hppі .tppфайлів ... але моєї пам'яті не вистачало для асоціації - а може, Я заблокував це. : D На щастя, з тих пір я переробив цю ділянку набагато менше, оскільки, як ви вказали, це, мабуть, рідко потрібна практика.
underscore_d

11

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

Тим не менш, є випадки, коли у нас є кілька класів в одному файлі. І саме тоді це просто приватний допоміжний клас для основного класу файлу.


13
ви дійсно не повинні мати 5000 класів в одному проекті <- Помилка .. чому б і ні? Звичайно, ви не хочете постійно їх перебудовувати, але якщо система така велика, то вона така велика.
Billy ONeal

5
"... ви дійсно не повинні мати 5000 класів в одному проекті". Це жахлива порада. Ви повинні мати багато занять, оскільки проект повинен бути достатньо ремонтопридатним.
birgersp

8

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


8

Окрім того, що просто «зрозуміліше», поділ класів на окремі файли полегшує багатьом розробникам не наступати один одному на пальці. Буде менше злиття, коли прийде час вносити зміни до вашого інструменту контролю версій.


1
Контроль версій - чудовий момент. Я б додав, що з рівноцінних причин переваги також поширюються на програмістів, яким рідко доводиться турбуватися про об'єднання, включаючи таких, як я, які кодують самостійно. Використання більших і менших файлів історію комітів набагато легше читати, будь то безпосередньо чи за допомогою, grepчи що інше. Наприклад, це набагато простіше набирати, git log SmallFile.cppніж git log GiganticFile.cpp | grep -b4 -a4 SureHopeIPutTheMagicWordInEveryCommitMsg(надуманий приклад, але ви розумієте).
underscore_d

5

Більшість місць, де я працював, дотримуються цієї практики. Я фактично написав стандарти кодування для BAE (Австрія) разом із причинами, чому замість того, щоб просто вирізати щось у камені без реального обґрунтування.

Щодо вашого запитання про вихідні файли, це не стільки часу на компіляцію, скільки проблема в тому, що спочатку можна знайти відповідний фрагмент коду. Не всі використовують IDE. А знання того, що ви просто шукаєте MyClass.h і MyClass.cpp, справді економить час порівняно із запуском "grep MyClass *. (H | cpp)" над купою файлів, а потім фільтруванням #include MyClass.h ...

Майте на увазі, є шляхи вирішення проблем впливу великої кількості вихідних файлів на час компіляції. Цікаву дискусію див. У розділі «Розробка програмного забезпечення C ++» від Джона Лакоса.

Можливо, ви також хотіли б прочитати Code Complete від Стіва Макконнелла, де ви знайдете чудовий розділ з інструкцій з кодування. Насправді, ця книга - чудове читання, до якого я постійно повертаюся


3

Це звичайна практика, особливо, щоб мати можливість включати .h у файли, які це потребують. Звичайно, це впливає на продуктивність, але намагайтеся не думати про цю проблему, доки вона не з’явиться :).
Краще почати з відокремлених файлів, а після цього спробувати об’єднати .h, які зазвичай використовуються разом, для підвищення продуктивності, якщо вам це дійсно потрібно. Все зводиться до залежностей між файлами, і це дуже специфічно для кожного проекту.


3

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



1

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


1

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

Я особисто групую пов'язані класи в окремі файли, а потім надаю такому файлу значуще ім'я, яке не доведеться змінювати, навіть якщо ім'я класу змінюється. Наявність меншої кількості файлів також полегшує прокрутку дерева файлів. Я використовую Visual Studio у Windows та Eclipse CDT у Linux, і обидва мають комбінації клавіш, які ведуть безпосередньо до декларації класу, тому пошук декларації класу простий та швидкий.

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

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


AFAICT - це повна проблема, пов’язана зі зміною групи імен файлів у компетентному середовищі розробника. Я маю на увазі, що в оболонці ви просто приймаєте команду, яку ви вже використовували для заміни імен у файлах, і трохи змінюєте її: sed -i 's/\<TheClassName\>/TheNewName/g' In*.?pp=> rename 's/\<TheClassName\>/TheNewName/' In*.?pp. Повторіть за парою підпунктів, якщо це необхідно. І якщо це sedспрацювало без конфліктів (або ви написали більш точний), то renameце швидше за все. Я майже не маю досвіду роботи з графічними інтерфейсами середовища розробки, але я сподіваюся, що це ще простіше, тому я не бачу проблеми.
underscore_d

0

Тут застосовується те саме правило, але воно зазначає кілька винятків, де це дозволено Подобається так:

  • Спадкові дерева
  • Класи, які використовуються лише в дуже обмеженому обсязі
  • Деякі утиліти просто розміщуються в загальному 'utils.h'

0

Два слова: Бритва Оккама. Зберігайте по одному класу на файл із відповідним заголовком в окремому файлі. Якщо ви робите інакше, наприклад, зберігаючи частину функціональності на файл, то вам доведеться створити всілякі правила щодо того, що становить частину функціональності. Ви можете отримати набагато більше, зберігаючи один клас на файл. І навіть наполовину пристойні інструменти можуть обробляти велику кількість файлів. Нехай це буде просто, друже.

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