C++ код в заголовочных файлах
Мой личный стиль в C++ всегда заключался в том, чтобы помещать объявления классов во включаемый файл, а определения - в файл.cpp, очень похоже на то, как это предусмотрено в ответе Локи на файлы заголовков C++, разделение кода. По общему признанию, одна из причин, по которой мне нравится этот стиль, вероятно, связана со всеми годами, которые я потратил на программирование Modula-2 и Ada, оба из которых имеют похожую схему с файлами спецификаций и файлами тела.
У меня есть коллега, гораздо более осведомленный в C++, чем я, который настаивает на том, чтобы все объявления C++, по возможности, включали определения прямо в заголовочный файл. Он не говорит, что это допустимый альтернативный стиль или даже немного лучший стиль, а скорее это новый общепринятый стиль, который все сейчас используют для C++.
Я не такой подвижный, как раньше, так что я не очень-то стремлюсь забраться на его подножку, пока не увижу с ним еще нескольких человек. Так насколько распространена эта идиома на самом деле?
Просто для того, чтобы дать некоторую структуру ответам: "Является ли это сейчас The Way, очень распространенным, несколько распространенным, необычным или ненормальным?
17 ответов
Ваш коллег не прав, обычным способом всегда было поместить код в файлы.cpp (или любое другое расширение по вашему желанию) и объявления в заголовках.
Иногда есть смысл помещать код в заголовок, это может позволить более умному встраиванию компилятором. Но в то же время это может разрушить время компиляции, поскольку весь код должен обрабатываться каждый раз, когда он включается компилятором.
Наконец, часто раздражает наличие циклических объектных отношений (иногда желаемых), когда весь код является заголовками.
Итог, вы были правы, он не прав.
РЕДАКТИРОВАТЬ: Я думал о вашем вопросе. Есть один случай, когда он говорит правду. шаблоны. Многие новые "современные" библиотеки, такие как boost, интенсивно используют шаблоны и часто являются "только заголовками". Однако это следует делать только при работе с шаблонами, поскольку это единственный способ сделать это при работе с ними.
РЕДАКТИРОВАТЬ: Некоторые люди хотели бы немного больше разъяснений, вот некоторые мысли о минусах написания кода "только заголовок":
Если вы поищете, вы увидите, что довольно много людей пытаются найти способ сократить время компиляции при работе с boost. Например: Как сократить время компиляции с Boost Asio, который видит компиляцию 14 с одного файла 1K с включенным Boost. Может показаться, что 14-ые не "взрываются", но они, безусловно, намного длиннее обычных и могут складываться довольно быстро. При работе с крупным проектом. Библиотеки только с заголовками действительно влияют на время компиляции. Мы просто терпим это, потому что повышение очень полезно.
Кроме того, есть много вещей, которые нельзя сделать только в заголовках (даже в boost есть библиотеки, на которые нужно ссылаться для определенных частей, таких как потоки, файловая система и т. Д.). Основным примером является то, что вы не можете иметь простые глобальные объекты в заголовках только libs (если вы не прибегаете к мерзости, которая является одиночной), так как вы столкнетесь с множественными ошибками определения. ПРИМЕЧАНИЕ: встроенные переменные C++17 сделают этот конкретный пример выполнимым в будущем.
И, наконец, при использовании boost в качестве примера кода, содержащего только заголовки, часто пропускаются огромные детали.
Boost - это библиотека, а не код пользовательского уровня. так что это не так часто меняется. В пользовательском коде, если вы помещаете все в заголовки, каждое небольшое изменение заставит вас перекомпилировать весь проект. Это огромная трата времени (и это не относится к библиотекам, которые не переходят от компиляции к компиляции). Когда вы делите вещи между заголовком / источником и, еще лучше, с помощью предварительных объявлений, чтобы уменьшить количество включений, вы можете сэкономить часы перекомпиляции при суммировании в течение дня.
В тот день, когда программисты на С ++ договорились о "Пути", ягнята присядет со львами, палестинцы примут израильтян, а кошкам и собакам разрешат жениться.
Разделение между файлами.h и.cpp в большинстве случаев является произвольным, пережитком давно прошедших оптимизаций компилятора. На мой взгляд, объявления принадлежат заголовку, а определения принадлежат файлу реализации. Но это просто привычка, а не религия.
Код в заголовках, как правило, плохая идея, так как он вызывает перекомпиляцию всех файлов, содержащих заголовок, когда вы изменяете фактический код, а не объявления. Это также замедлит компиляцию, так как вам нужно будет анализировать код в каждом файле, который содержит заголовок.
Причиной наличия кода в заголовочных файлах является то, что обычно требуется, чтобы ключевое слово inline работало должным образом и при использовании шаблонов, которые встраиваются в другие файлы cpp.
Что может быть полезным для вас, коллега, так это представление о том, что большая часть кода C++ должна быть спроектирована так, чтобы обеспечить максимальное удобство использования. И если это шаблонно, то все должно быть в заголовочном файле, чтобы клиентский код мог его увидеть и создать экземпляр. Если это достаточно хорошо для Boost и STL, это достаточно хорошо для нас.
Я не согласен с этой точкой зрения, но это может быть то, откуда это исходит.
Я думаю, что ваш коллега умен и вы тоже правы.
Полезные вещи, которые я обнаружил, помещают все в заголовки так:
Нет необходимости писать и синхронизировать заголовки и источники.
Структура проста, и никакие циклические зависимости не заставляют кодера создавать "лучшую" структуру.
Портативный, легко встраивается в новый проект.
Я согласен с проблемой времени компиляции, но я думаю, что мы должны заметить, что:
Смена исходного файла, скорее всего, изменит заголовочные файлы, что приведет к повторной компиляции всего проекта.
Скорость компиляции намного быстрее, чем раньше. И если у вас есть проект, который будет создаваться с большой периодичностью и высокой частотой, это может указывать на недостатки вашего проекта. Разделите задачи на разные проекты, и модуль может избежать этой проблемы.
Наконец, я просто хочу поддержать твоего коллегу, просто по моему личному мнению.
Часто я помещаю тривиальные функции-члены в заголовочный файл, чтобы позволить им быть встроенными. Но разместить там весь код, чтобы соответствовать шаблонам? Это просто орехи.
Помните: глупая последовательность - это хобгоблин маленьких умов.
Как сказал Туомас, ваш заголовок должен быть минимальным. Чтобы быть полным, я расширю немного.
Я лично использую 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
и так же заголовок реализации;)
Я лично делаю это в моих заголовочных файлах:
// class-declaration
// inline-method-declarations
Мне не нравится смешивать код для методов с классом, так как мне сложно быстро что-то искать.
Я бы не помещал ВСЕ методы в заголовочный файл. Компилятор (обычно) не сможет встроить виртуальные методы и (вероятно) только встроенные небольшие методы без циклов (полностью зависит от компилятора).
Использование методов в классе допустимо... но с точки зрения читабельности мне это не нравится. Помещение методов в заголовок означает, что, когда это возможно, они будут встроены.
Чтобы добавить больше удовольствия, вы можете добавить .ipp
файлы, которые содержат реализацию шаблона (которая включена в .hpp
), в то время как .hpp
содержит интерфейс
Помимо шаблонного кода (в зависимости от проекта это может быть большинство или меньшая часть файлов), существует нормальный код, и здесь лучше разделить объявления и определения. При необходимости укажите также предварительные объявления - это может повлиять на время компиляции.
Обычно при написании нового класса я помещаю весь код в класс, поэтому мне не нужно искать его в другом файле. После того, как все работает, я разбиваю тело методов в файл cpp. оставив прототипы в файле hpp.
Я думаю, что абсолютно абсурдно помещать ВСЕ ваши определения функций в заголовочный файл. Зачем? Потому что заголовочный файл используется как интерфейс PUBLIC для вашего класса. Это за пределами "черного ящика".
Когда вам нужно посмотреть на класс, чтобы узнать, как его использовать, вы должны посмотреть на файл заголовка. Заголовочный файл должен давать список того, что он может делать (закомментированный, чтобы описать детали того, как использовать каждую функцию), и он должен включать список переменных-членов. Он НЕ ДОЛЖЕН включать КАК реализована каждая отдельная функция, потому что это лишний груз ненужной информации и только загромождает заголовочный файл.
Если этот новый путь действительно The Way, мы могли бы столкнуться в другом направлении в наших проектах.
Потому что мы стараемся избегать всех ненужных вещей в заголовках. Это включает в себя избегание каскада заголовков. Код в заголовках, вероятно, будет нуждаться во включении другого заголовка, который будет нуждаться в другом заголовке и так далее. Если мы вынуждены использовать шаблоны, мы стараемся избегать засорения заголовков шаблоном.
Также мы используем шаблон "непрозрачный указатель", когда это применимо.
С помощью этих методов мы можем делать более быстрые сборки, чем большинство наших коллег. И да... изменение кода или членов класса не приведет к огромным перестройкам.
Я положил всю реализацию из определения класса. Я хочу, чтобы комментарии doxygen были вне определения класса.
ИМХО, он достоин ТОЛЬКО если он занимается шаблонами и / или метапрограммированием. Уже упоминалось множество причин, по которым вы ограничиваете заголовочные файлы только объявлениями. Они просто... заголовки. Если вы хотите включить код, вы собираете его как библиотеку и связываете его.
Разве это не зависит от сложности системы и внутренних соглашений?
В данный момент я работаю над симулятором нейронной сети, который невероятно сложен, и принятый стиль, который я должен использовать:
Определения классов в classname.h
Код класса в classnameCode.h
исполняемый код в classname.cpp
Это отделяет пользовательское моделирование от базовых классов, разработанных разработчиком, и лучше всего работает в данной ситуации.
Тем не менее, я был бы удивлен, увидев, что люди делают это, скажем, в графическом приложении или любом другом приложении, целью которого не является предоставление пользователям базы кода.
Я думаю, что ваш коллега прав, если он не входит в процесс, чтобы написать исполняемый код в заголовке. Правильный баланс, я думаю, должен следовать по пути, указанному в GNAT Ada, где файл.ads дает совершенно адекватное определение интерфейса пакета для его пользователей и его дочерних элементов.
Кстати, Тед, вы смотрели на этом форуме недавний вопрос о привязке Ada к библиотеке CLIPS, которую вы написали несколько лет назад и которая больше не доступна (соответствующие веб-страницы теперь закрыты). Даже если она сделана для старой версии Clips, эта привязка может стать хорошим примером для кого-то, кто хочет использовать механизм вывода CLIPS в программе Ada 2012.
Код шаблона должен быть только в заголовках. Кроме того, все определения, кроме строк, должны быть в.cpp. Лучшим аргументом для этого будут реализации библиотеки std, которые следуют тому же правилу. Вы не согласитесь, что разработчики std lib будут правы в этом отношении.