Чи є гарною практикою розміщення визначень C ++ у файлах заголовків?


196

Мій особистий стиль із C ++ завжди повинен містити декларації класу у файлі include, а визначення - у файлі .cpp, дуже схожому на відповідь Локі на файли заголовків C ++, розділення коду . Щоправда, частина причин, що мені подобається цей стиль, мабуть, пов’язана з усіма роками, які я проводив у кодуванні Modula-2 та Ada, обидві з яких мають схожу схему з файлами специфікацій та файлами тіла

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

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

Просто, щоб дати певну структуру відповідей: Це зараз Шлях , дуже поширений, дещо поширений, нечастий або божевільний з розумом?


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

Я завжди ставив усі визначення мого класу в заголовках. Винятки становлять лише визначення для класів pimpl. я оголошую лише це в заголовках.
Йоханнес Шауб - ліб

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

4
@WKS - Microsoft, швидше за все, програмує на C #, а в C # немає відмінності "заголовка" проти "тіла", це лише один файл. Перебуваючи в C ++ і C # світах вже давно, шлях C # насправді набагато простіше подолати.
Марк Лаката

1
@MarkLakata - Це дійсно одна з речей, на яку він вказував. Я не чув цього аргументу від нього останнім часом, але в IIRC він стверджував, що Java та C # працюють таким чином, і C # був абсолютно новим у той час, що зробило тенденцію, що всі мови незабаром будуть дотримуватися
TED

Відповіді:


209

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

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

Нарешті, часто дратує наявність кругових зв'язків об’єктів (іноді бажаних), коли весь код є заголовками.

Підсумок, ви мали рацію, він помиляється.

EDIT: Я думав над вашим запитанням. Є один випадок, коли те, що він говорить, є правдою. шаблони. Багато нових "сучасних" бібліотек, таких як boost, сильно використовують шаблони і часто є "лише заголовком". Однак це слід робити лише при роботі із шаблонами, оскільки це єдиний спосіб зробити це при роботі з ними.

РЕДАКТУВАННЯ. Деякі люди хотіли б трохи роз’яснити, ось деякі думки щодо недоліків написання коду «тільки в заголовку»:

Якщо ви обшукуєте, ви побачите досить багато людей, які намагаються знайти спосіб зменшити час компіляції при роботі зі збільшенням. Наприклад: Як зменшити час компіляції за допомогою Boost Asio , який бачить компіляцію з 14-х годин одного файлу 1К з включеним прискоренням. 14-і можуть не здаватися "вибухаючими", але це, звичайно, набагато довше, ніж типові, і може скластися досить швидко. Маючи справу з великим проектом. Тільки бібліотеки заголовків впливають на час компіляції досить вимірюваним чином. Ми просто терпимо це, тому що підвищення настільки корисне.

Крім того, є багато речей, які неможливо виконати лише в заголовках (навіть прискорення має бібліотеки, до яких потрібно зв’язати певні частини, такі як потоки, файлова система тощо). Основний приклад - це те, що ви не можете мати простих глобальних об'єктів у заголовках, що містять лише заголовки (якщо тільки ви не вдаєтесь до огиди, яка є однотонною), оскільки ви зіткнетесь з помилками численних визначень.ПРИМІТКА . Вбудовані змінні C ++ 17 зроблять цей конкретний приклад в майбутньому.

Нарешті, під час використання boost як приклад коду лише заголовка, величезна деталь часто пропускається.

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


16
Я майже впевнений, що звідки він це отримує. Коли це з'являється, він відкриває шаблони. Його аргумент приблизно полягає в тому, що ви повинні робити весь код таким чином для послідовності.
ТЕД

14
це поганий аргумент, який він робить, дотримуйтесь ваших гармат :)
Еван Теран

11
Визначення шаблонів можуть бути у файлах CPP, якщо підтримується ключове слово "експорт". Це темний куточок C ++, який зазвичай навіть не реалізований більшістю компіляцій, наскільки мені відомо.
Андрій Таранченко

2
Дивіться нижню частину цієї відповіді (верх дещо перекручений) для прикладу: stackoverflow.com/questions/555330/…
Еван Теран

3
Починає бути значимим для цієї дискусії з "Хураю, помилок лінкера немає".
Еван Теран

158

У той день, коли кодери C ++ домовляться про The Way , ягняти лягатимуть із левами, палестинці приймуть ізраїльтян, а котам та собакам буде дозволено одружуватися.

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


140
"Дня, коли C ++ кодери домовляться про The Way ..." залишиться лише один C ++ кодер!
Брайан Енсінк

9
Я подумав, що вони вже домовляються про спосіб, декларації в .h та визначення у .cpp
hasen

6
Всі ми сліпі люди, а С ++ - слон.
Родерік Тейлор

звичка? так що з використанням .h для визначення області? на яку річ вона була замінена?
Ернан Еш

28

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

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


1
"Це змушує перекомпіляцію всіх файлів, що включає заголовок, коли ви змінюєте фактичний код, а не декларації". Я думаю, що це найбільш справжня причина; також пов'язано з тим, що декларації в заголовках змінюються рідше, ніж реалізація у файлах .c
Нінад

20

Що може сповістити ваших колег - це думка, що більшість кодів C ++ має бути розроблено, щоб забезпечити максимальну зручність використання. І якщо він шаблонований, тоді все повинно бути у файлі заголовка, щоб клієнтський код міг бачити його та інстанціювати його. Якщо це досить добре для Boost та STL, це досить добре для нас.

Я не згоден з цією точкою зору, але це може бути звідки вона походить.


Я думаю, ти маєш рацію з цього приводу. Коли ми це обговорюємо, він завжди використовує приклад шаблонів, де вам це потрібно зробити більш-менш . Я також не погоджуюся з "доведеться", але мої альтернативи доволі суперечливі.
ТЕД

1
@ted - для шаблонного коду вам потрібно помістити реалізацію в заголовок. Ключове слово "експортувати" дозволяє компілятору підтримувати розділення декларації та визначення шаблонів, але підтримка експорту є досить значною мірою. anubis.dkuug.dk/jtc1/sc22/wg21/docs/papers/2003/n1426.pdf
Майкл Берр

Тема, так, але він не повинен бути таким же заголовком. Дивіться відповідь невідомого нижче.
ТЕД

Це має сенс, але я не можу сказати, що раніше я стикався з цим стилем.
Майкл Берр

14

Я думаю, що ваш колега розумний, і ви також правильно.

Корисні речі я виявив, що вкладаючи все в заголовки, це те, що:

  1. Не потрібно писати та синхронізувати заголовки та джерела.

  2. Структура є простою, і ніякі кругові залежності не змушують кодер робити "кращу" структуру.

  3. Портативний, легко вбудовується в новий проект.

Я погоджуюся з проблемою складання часу, але я думаю, що ми повинні помітити це:

  1. Зміна вихідного файлу, швидше за все, змінить заголовкові файли, що призводить до того, що весь проект буде перекомпільований заново.

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

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


3
+1. Ніхто, крім вас, не мав ідеї, що в заголовку лише тривалий час компіляції проекту може натякати на занадто великі залежності, що є поганим дизайном. Гарна думка! Але чи можна усунути ці залежності до такої міри, коли часу на компіляцію насправді не вистачає?
TobiMcNamobi

@TobiMcNamobi: Мені подобається ідея "слабкості", щоб отримати кращі відгуки про погані дизайнерські рішення. Однак у випадку заголовка проти окремо складеного, якщо ми будемо дотримуватися цієї ідеї, ми закінчимося єдиним блоком компіляції та величезним часом компіляції. Навіть коли дизайн насправді чудовий.
Jo So

Іншими словами, розділення між інтерфейсом та реалізацією насправді є частиною вашого дизайну. В мові ви повинні висловити свої рішення щодо інкапсуляції шляхом розділення в заголовку та реалізації.
Jo So

1
Я починаю замислюватися, чи є якісь недоліки, щоб взагалі просто скинути заголовки, як це роблять сучасні мови.
Jo So

12

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

Пам’ятайте: Нерозумна консистенція - це хобгоблін маленьких розумів .


Так, я теж це роблю. Загальне правило, яке я використовую, здається, є чимось у рядку "якщо він вписується в один рядок коду, залиште його в заголовку".
ТЕД

Що відбувається, коли бібліотека надає тіло класу шаблонів A<B>у файлі cpp, і тоді користувач бажає A<C>?
jww

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

7

Як сказав Туомас, ваш заголовок повинен бути мінімальним. Для завершення я трохи розширюся.

Я особисто використовую 4 типи файлів у своїх C++проектах:

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

Крім того, я поєдную це з іншим правилом: Не визначайте, що можна переслати декларувати. Хоча, звичайно, я розумний (використовувати Pimpl скрізь - це досить складно).

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

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

Вклавши це повністю:

// example_fwd.hpp
// Here necessary to forward declare the template class,
// you don't want people to declare them in case you wish to add
// another template symbol (with a default) later on
class MyClass;
template <class T> class MyClassT;

// example.hpp
#include "project/example_fwd.hpp"

// Those can't really be skipped
#include <string>
#include <vector>

#include "project/pimpl.hpp"

// Those can be forward declared easily
#include "project/foo_fwd.hpp"

namespace project { class Bar; }

namespace project
{
  class MyClass
  {
  public:
    struct Color // Limiting scope of enum
    {
      enum type { Red, Orange, Green };
    };
    typedef Color::type Color_t;

  public:
    MyClass(); // because of pimpl, I need to define the constructor

  private:
    struct Impl;
    pimpl<Impl> mImpl; // I won't describe pimpl here :p
  };

  template <class T> class MyClassT: public MyClass {};
} // namespace project

// example_impl.hpp (not visible to clients)
#include "project/example.hpp"
#include "project/bar.hpp"

template <class T> void check(MyClass<T> const& c) { }

// example.cpp
#include "example_impl.hpp"

// MyClass definition

Рятувальник тут полягає в тому, що більшість разів передній заголовок є марним: необхідний лише у випадку typedefабо templateі так є заголовок реалізації;)


6

Щоб додати більше задоволення, ви можете додати .ippфайли, які містять реалізацію шаблону (що входить до нього .hpp), а .hppмістять інтерфейс.

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


Це те, що я брав до роботи з визначеннями шаблонів (хоча я не впевнений, що використовував те саме розширення ... минув час).
ТЕД

5

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


4

Якщо цей новий шлях насправді Шлях , ми, можливо, в своїх проектах стикалися в іншому напрямку.

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

Також ми використовуємо "непрозорий вказівник" - шаблон коли це застосовується.

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


4

Я особисто роблю це у своїх заголовкових файлах:

// class-declaration

// inline-method-declarations

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

Я б не ставив ВСІХ методів у файл заголовка. Компілятор (як правило) не зможе вбудовувати віртуальні методи і, ймовірно, буде лише вбудовувати невеликі методи без циклів (повністю залежить від компілятора).

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


2

IMHO, Він має заслугу ТОЛЬКО, якщо він робить шаблони та / або метапрограмування. Існує безліч причин, про які вже згадувалося, що ви обмежуєте файли заголовків лише деклараціями. Вони просто такі ... заголовки. Якщо ви хочете включити код, ви компілюєте його як бібліотеку і пов'язуєте його.


2

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


1
Я знаю, що вже пізно, але прихильники (або співчуваючі) дбають коментувати, чому? Це здається мені розумним твердженням. Ми використовуємо Doxygen, і проблема, безумовно, виникла.
ТЕД

2

Я вважаю, що абсолютно абсурдно розміщувати ВСІ свої визначення функції у файлі заголовка. Чому? Оскільки файл заголовка використовується як інтерфейс PUBLIC для вашого класу. Це зовні "чорний ящик".

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


1

Чи насправді це не залежить від складності системи та внутрішніх конвенцій?

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

Визначення класів у classname.h
Код класу у classnameCode.h
виконуваний код у classname.cpp

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

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


1
У чому саме полягає відмінність "Код класу" від "Виконавчого коду"?
ТЕД,

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

Взагалі, чи не могли б ви сказати, що "фактично нічого не можна зробити самому" для переважної більшості (якщо не в цілому) більшості будь-якої програми? Ви хочете сказати, що "основний" код входить в cpp, але нічого іншого не робить?
ТЕД

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

0

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


Які стдліби? libstdc++Здається, GCC (AFAICS) не вкладає майже нічого в src& майже все include, незалежно від того, чи повинен він бути "в" заголовку. Тому я не думаю, що це точне / корисне цитування. У будь-якому випадку, я не думаю, що stdlibs - це не дуже модель для коду користувача: вони, очевидно, написані висококваліфікованими кодерами, але для їх використання не читаються: вони абстрагують високу складність, про яку більшості кодерів не потрібно думати. , потрібно некрасиво _Reserved __namesскрізь, щоб уникнути конфліктів з користувачем, коментарі та пробіли знаходяться нижче того, що я б порадив тощо.
підкреслити_16

0

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

До речі, Тед, ви заглядали на цьому форумі на недавнє запитання про прив'язку Ада до бібліотеки CLIPS, про яке ви писали кілька років тому, і яке більше недоступне (відповідні веб-сторінки закриті). Навіть якщо це зроблено для старої версії Clips, це прив'язка може стати хорошим прикладом для того, хто бажає використовувати двигун висновку CLIPS в рамках програми Ada 2012.


1
Лол. Через 2 роки це дивний спосіб добути когось. Я перевірю, чи є у мене ще копія, але, швидше за все, ні. Я зробив це для класу AI, щоб я міг зробити свій код на Ada, але навмисно зробив цей проект CC0 (по суті, захищений авторським правом), сподіваючись, що хтось безсоромно візьме його і щось зробить з ним.
ТЕД
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.