Полезно ли размещать определения C ++ в заголовочных файлах?

196

Мой личный стиль в C ++ всегда заключался в том, чтобы помещать объявления классов во включаемый файл, а определения - в файл .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 (или любое другое расширение, которое вам нравится) и объявления в заголовках.

Иногда есть смысл помещать код в заголовок, это может позволить более умному встраиванию компилятором. Но в то же время это может разрушить время компиляции, поскольку весь код должен обрабатываться каждый раз, когда он включается компилятором.

Наконец, часто раздражает наличие циклических объектных отношений (иногда желаемых), когда весь код является заголовками.

Итог, вы были правы, он не прав.

РЕДАКТИРОВАТЬ: я думал о вашем вопросе. Есть один случай, когда то, что он говорит, правда. шаблоны. Многие новые «современные» библиотеки, такие как boost, интенсивно используют шаблоны и часто являются «только заголовками». Однако это следует делать только при работе с шаблонами, поскольку это единственный способ сделать это при работе с ними.

РЕДАКТИРОВАТЬ: Некоторые люди хотели бы немного больше разъяснений, вот некоторые мысли о минусах написания кода "только заголовок":

Если вы будете искать по всему, вы увидите, что довольно много людей пытаются найти способ сократить время компиляции при работе с boost. Например: Как сократить время компиляции с Boost Asio , который видит компиляцию 14 с одного файла 1K с включенным Boost . Может показаться, что 14-ые не «взрываются», но они, безусловно, намного дольше, чем обычно, и могут складываться довольно быстро. При работе с крупным проектом. Библиотеки только с заголовками действительно влияют на время компиляции. Мы просто терпим это, потому что повышение очень полезно.

Кроме того, есть много вещей, которые нельзя сделать только в заголовках (даже в boost есть библиотеки, на которые нужно ссылаться для определенных частей, таких как потоки, файловая система и т. Д.). Основным примером является то, что вы не можете иметь простые глобальные объекты в заголовках только libs (если вы не прибегаете к мерзости, которая является одиночной), так как вы столкнетесь с множественными ошибками определения. ПРИМЕЧАНИЕ: встроенные переменные C ++ 17 сделают этот конкретный пример выполнимым в будущем.

И наконец, при использовании boost в качестве примера кода, содержащего только заголовки, часто пропускаются огромные детали.

Boost - это библиотека, а не код пользовательского уровня. так что это не так часто меняется. В пользовательском коде, если вы помещаете все в заголовки, каждое небольшое изменение заставит вас перекомпилировать весь проект. Это огромная трата времени (и это не относится к библиотекам, которые не переходят от компиляции к компиляции). Когда вы делите вещи между заголовком / источником и, еще лучше, с помощью предварительных объявлений, чтобы уменьшить количество включений, вы можете сэкономить часы перекомпиляции при суммировании в течение дня.

Эван Теран
источник
16
Я почти уверен, что откуда он это взял. Когда бы это ни возникало, он выводит шаблоны. Его аргумент примерно таков, что вы должны делать весь код таким образом для согласованности.
TED
14
это плохой аргумент, который он выдвигает, придерживайся своего оружия :)
Эван Теран
11
Определения шаблонов могут быть в файлах CPP, если поддерживается ключевое слово «export». Насколько мне известно, это темный угол C ++, который обычно даже не реализуется большинством компиляторов.
Андрей Таранченко
2
См. Нижнюю часть этого ответа (верхняя часть несколько запутанная) для примера: stackoverflow.com/questions/555330/…
Эван Теран
3
Это становится значимым для этого обсуждения на «Ура, никаких ошибок компоновщика».
Эван Теран
158

В тот день, когда программисты на С ++ договорились о «Пути» , ягнята присядет со львами, палестинцы примут израильтян, а кошкам и собакам разрешат жениться.

Разделение между файлами .h и .cpp в большинстве случаев является произвольным, пережитком давно прошедших оптимизаций компилятора. На мой взгляд, объявления принадлежат заголовку, а определения принадлежат файлу реализации. Но это просто привычка, а не религия.

Да, это Джейк.
источник
140
«В тот день, когда кодеры C ++ договариваются о пути ...» останется только один кодер C ++!
Брайан Энсинк
9
Я думал, что они уже договорились о пути, объявлениях в .h и определениях в .cpp
hasen
6
Мы все слепые, а С ++ - это слон.
Родерик Тейлор
привычка? так что насчет использования .h для определения области? на что это было заменено?
Эрнан Эче
28

Код в заголовках, как правило, плохая идея, так как он вызывает перекомпиляцию всех файлов, содержащих заголовок, когда вы изменяете фактический код, а не объявления. Это также замедлит компиляцию, так как вам нужно будет анализировать код в каждом файле, который содержит заголовок.

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

Laserallan
источник
1
«это вызывает перекомпиляцию всех файлов, которые содержат заголовок, когда вы изменяете фактический код, а не объявления». Я думаю, что это самая настоящая причина; также связано с тем, что объявления в заголовках изменяются реже, чем в файлах .c.
Ninad
20

Что может быть полезным для вас, коллега, так это представление о том, что большая часть кода C ++ должна быть спроектирована так, чтобы обеспечить максимальное удобство использования. И если он является шаблонным, то все должно быть в заголовочном файле, чтобы клиентский код мог его увидеть и создать экземпляр. Если это достаточно хорошо для Boost и STL, это достаточно хорошо для нас.

Я не согласен с этой точкой зрения, но это может быть то, откуда это исходит.

JohnMcG
источник
Я думаю, что вы правы по этому поводу. Когда мы обсуждаем это, он всегда использует пример шаблонов, где вы более или менее должны это сделать. Я также не согласен с «должен», но мои альтернативы довольно запутанны.
TED
1
@ted - для шаблонного кода вам нужно поместить реализацию в заголовок. Ключевое слово «export» позволяет компилятору поддерживать разделение объявлений и определений шаблонов, но поддержка экспорта практически отсутствует. anubis.dkuug.dk/jtc1/sc22/wg21/docs/papers/2003/n1426.pdf
Майкл Барр
Заголовок, да, но он не должен быть таким же заголовком. Смотрите неизвестный ответ ниже.
TED
Это имеет смысл, но я не могу сказать, что сталкивался с этим стилем раньше.
Майкл Берр
14

Я думаю, что ваш коллега умен и вы тоже правы.

Полезные вещи, которые я обнаружил, помещают все в заголовки так:

  1. Нет необходимости писать и синхронизировать заголовки и источники.

  2. Структура проста, и никакие циклические зависимости не заставляют кодера создавать «лучшую» структуру.

  3. Портативный, легко встраивается в новый проект.

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

  1. Смена исходного файла, скорее всего, изменит заголовочные файлы, что приведет к повторной компиляции всего проекта.

  2. Скорость компиляции намного быстрее, чем раньше. И если у вас есть проект, который будет создаваться с длительной и высокой частотой, это может указывать на недостатки вашего проекта. Разделите задачи на разные проекты, и модуль может избежать этой проблемы.

Наконец, я просто хочу поддержать твоего коллегу, просто по моему личному мнению.

XU Bin
источник
3
+1. Никто, кроме вас, не думал, что в проекте только заголовка длительное время компиляции может указывать на слишком много зависимостей, что является плохим дизайном. Хорошая точка зрения! Но можно ли удалить эти зависимости до такой степени, что время компиляции на самом деле мало?
TobiMcNamobi
@TobiMcNamobi: Мне нравится идея "расслабиться", чтобы получить лучшую обратную связь о плохих дизайнерских решениях. Однако в случае только с заголовками и отдельно скомпилированными, если мы остановимся на этой идее, мы получим одну единицу компиляции и огромное время компиляции. Даже когда дизайн на самом деле великолепен.
Джо Со
Другими словами, разделение между интерфейсом и реализацией на самом деле является частью вашего дизайна. В C вы обязаны выражать свои решения по инкапсуляции через разделение в заголовке и реализации.
Джо Со
1
Я начинаю задаваться вопросом, есть ли какие-то недостатки вообще, чтобы просто отбрасывать заголовки, как это делают современные языки.
Джо Со
12

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

Помните: глупая последовательность - это хобгоблин маленьких умов .

Марк Рэнсом
источник
Да, я тоже так делаю. Кажется, что общее правило, которое я использую, звучит примерно так: «если оно умещается в одной строке кода, оставьте его в заголовке».
TED
Что происходит, когда библиотека предоставляет тело класса шаблона A<B>в файле cpp, а затем пользователь хочет получить A<C>?
17
@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содержит интерфейс.

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

анонимное
источник
Это то, что я тоже сделал с определениями шаблонов (хотя я не уверен, что использовал то же расширение ... это было давно).
TED
5

Как правило, при написании нового класса я помещаю весь код в класс, поэтому мне не нужно искать его в другом файле. После того, как все работает, я разбиваю тело методов в файл cpp. оставив прототипы в файле hpp.

EvilTeach
источник
4

Если этот новый путь действительно The Way , мы могли бы столкнуться в другом направлении в наших проектах.

Потому что мы стараемся избегать всех ненужных вещей в заголовках. Это включает в себя избегание каскада заголовков. Код в заголовках, вероятно, будет нуждаться во включении другого заголовка, который будет нуждаться в другом заголовке и так далее. Если мы вынуждены использовать шаблоны, мы стараемся избегать засорения заголовков шаблоном.

Также мы используем шаблон «непрозрачный указатель», когда это применимо.

С помощью этих методов мы можем делать более быстрые сборки, чем большинство наших коллег. И да ... изменение кода или членов класса не приведет к огромным перестройкам.

Virne
источник
4

Я лично делаю это в моих заголовочных файлах:

// class-declaration

// inline-method-declarations

Мне не нравится смешивать код для методов с классом, так как мне сложно быстро все искать.

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

Использование методов в классе допустимо ... но с точки зрения читабельности мне это не нравится. Помещение методов в заголовок означает, что, когда это возможно, они будут встроены.

TofuBeer
источник
2

ИМХО, он достоин ТОЛЬКО если он занимается шаблонами и / или метапрограммированием. Уже упоминалось множество причин, по которым вы ограничиваете заголовочные файлы только объявлениями. Они просто ... заголовки. Если вы хотите включить код, вы компилируете его как библиотеку и связываете его.

spoulson
источник
2

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

Хесус Фернандес
источник
1
Я знаю, что уже поздно, но даунвотеры (или сочувствующие) хотят прокомментировать, почему? Это кажется мне разумным утверждением. Мы используем Doxygen, и проблема, безусловно, возникла.
TED
2

Я думаю, что абсолютно абсурдно помещать ВСЕ ваши определения функций в заголовочный файл. Зачем? Потому что заголовочный файл используется как интерфейс PUBLIC для вашего класса. Это за пределами «черного ящика».

Когда вам нужно посмотреть на класс, чтобы узнать, как его использовать, вы должны посмотреть на заголовочный файл. Заголовочный файл должен давать список того, что он может делать (закомментированный, чтобы описать детали того, как использовать каждую функцию), и он должен включать список переменных-членов. Он НЕ ДОЛЖЕН включать КАК реализована каждая отдельная функция, потому что это лишний груз ненужной информации и только загромождает заголовочный файл.

SeanRamey
источник
1

Разве это не зависит от сложности системы и внутренних соглашений?

В данный момент я работаю над симулятором нейронной сети, который невероятно сложен, и приемлемый стиль, который я должен использовать:

Определения классов в classname.h
Код класса в classnameCode.h
исполняемый код в classname.cpp

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

Тем не менее, я был бы удивлен, увидев, что люди делают это, скажем, в графическом приложении или любом другом приложении, целью которого не является предоставление пользователям базы кода.

Эд Джеймс
источник
1
В чем именно заключается различие между «кодом класса» и «исполняемым кодом»?
TED
Как я уже сказал, это нейронный симулятор: пользователь создает исполняемые симуляции, которые построены на большом количестве классов, которые действуют как нейроны и т. Д. Таким образом, наш код - это просто классы, которые на самом деле ничего не могут сделать сами, а пользователь создает исполняемый код это заставляет симулятор делать вещи.
Эд Джеймс
Вообще, не могли бы вы сказать «не может ничего сделать на самом деле» для подавляющего большинства (если не полностью) большинства программ? Вы говорите, что «основной» код идет в cpp, но больше ничего не делает?
TED
В этой ситуации все немного по-другому. Код, который мы пишем, в основном является библиотекой, и пользователь строит свои симуляции поверх этого, которые на самом деле выполняются. Думайте об этом как openGL -> вы получаете кучу функций и объектов, но без файла cpp, который может их запускать, они бесполезны.
Эд Джеймс
0

Код шаблона должен быть только в заголовках. Кроме того, все определения, кроме строк, должны быть в .cpp. Лучшим аргументом для этого были бы реализации библиотеки std, которые следуют тому же правилу. Вы не согласитесь, что разработчики std lib будут правы в этом отношении.

Абхишек Агарвал
источник
Какие stdlibs? libstdc++Похоже, что GCC (AFAICS) почти ничего не вставляет srcи почти все include, независимо от того, должно ли это быть в заголовке. Поэтому я не думаю, что это точная / полезная цитата. В любом случае, я не думаю, что stdlibs - это большая часть модели для пользовательского кода: они, очевидно, написаны высококвалифицированными программистами, но для использования , а не для чтения: они абстрагируются от высокой сложности, о которой большинству кодеров не нужно думать Нужно безобразно _Reserved __namesвезде, чтобы избежать конфликтов с пользователем, комментарии и интервалы ниже того, что я бы посоветовал, и т. д. Они являются образцовыми в узком смысле.
underscore_d
0

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

Кстати, Тед, вы смотрели на этом форуме недавний вопрос о привязке Ada к библиотеке CLIPS, которую вы написали несколько лет назад и которая больше не доступна (соответствующие веб-страницы теперь закрыты). Даже если она сделана для старой версии Clips, эта привязка может стать хорошим примером для тех, кто хочет использовать механизм вывода CLIPS в программе Ada 2012.

Emile
источник
1
Ржунимагу. Спустя 2 года это странный способ кого-то заполучить. Я проверю, есть ли у меня копия, но, скорее всего, нет. Я сделал это для класса ИИ, чтобы я мог сделать свой код в Аде, но намеренно сделал этот проект CC0 (по сути, без авторских прав) в надежде, что кто-то бесстыдно возьмет его и что-то с ним сделает.
TED