Подтвердить что ты не робот

Плюсы и минусы размещения всего кода в файлах заголовков на С++?

Вы можете структурировать программу на С++, чтобы (почти) весь код находился в файлах заголовков. Он по сути выглядит как С# или Java-программа. Тем не менее, вам нужен хотя бы один .cpp файл, чтобы вытащить все файлы заголовков при компиляции. Теперь я знаю, что некоторые люди будут абсолютно ненавидеть эту идею. Но я не нашел убедительных недостатков в этом. Я могу указать некоторые преимущества:

[1] Более быстрое время компиляции. Все файлы заголовков обрабатываются только один раз, потому что есть только один .cpp файл. Кроме того, один заголовочный файл не может быть включен более одного раза, иначе вы получите разрыв сборки. Существуют другие способы достижения более быстрого компиляции при использовании альтернативного подхода, но это так просто.

[2] Он избегает круговых зависимостей, делая их абсолютно ясными. Если ClassA в ClassA.h имеет круговую зависимость от ClassB в ClassB.h, я должен поставить переднюю ссылку, и она торчит. (Обратите внимание, что это не похоже на С# и Java, где компилятор автоматически разрешает круговые зависимости, что поощряет неправильные методы кодирования IMO). Опять же, вы можете избежать круговых зависимостей, если ваш код находился в файлах .cpp, но в реальном проекте файлы .cpp имеют тенденцию включать случайные заголовки, пока вы не сможете определить, кто зависит от кого.

Ваши мысли?

4b9b3361

Ответ 1

Причина [1] Более быстрое время компиляции

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

Возможно, вам следует разбить свой проект на более логичные источники/заголовки: для модификации в реализации класса A НЕ нужна перекомпиляция реализаций классов B, C, D, E и т.д.

Причина [2] Он избегает круговых зависимостей

Круговые зависимости в коде?

Извините, но у меня еще есть такая проблема, которая является реальной проблемой: пусть говорят, что A зависит от B, а B зависит от A:

struct A
{
   B * b ;
   void doSomethingWithB() ;
} ;

struct B
{
   A * a ;
   void doSomethingWithA() ;
} ;

void A::doSomethingWithB() { /* etc. */ }
void B::doSomethingWithA() { /* etc. */ }

Хорошим способом решения проблемы является разбиение этого источника на по крайней мере один источник/заголовок для каждого класса (аналогично Java-способу, но с одним источником и одним заголовком для каждого класса):

// A.hpp

struct B ;

struct A
{
   B * b ;
   void doSomethingWithB() ;
} ;

.

// B.hpp

struct A ;

struct B
{
   A * a ;
   void doSomethingWithA() ;
} ;

.

// A.cpp
#include "A.hpp"
#include "B.hpp"

void A::doSomethingWithB() { /* etc. */ }

.

// B.cpp
#include "B.hpp"
#include "A.hpp"

void B::doSomethingWithA() { /* etc. */ }

Таким образом, нет проблемы с зависимостью и все еще быстрая время компиляции.

Я что-то пропустил?

При работе над проектами "real-world"

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

Конечно. Но если у вас есть время реорганизовать эти файлы для создания своего "одного CPP", у вас есть время для очистки этих заголовков. Мои правила для заголовков:

  • сломайте заголовок, чтобы сделать их как можно более модульными
  • Никогда не включайте заголовки, которые вам не нужны.
  • Если вам нужен символ, вперед-объявите его
  • только если вышеуказанное не удалось, включите заголовок

В любом случае все заголовки должны быть самодостаточными, что означает:

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

Это устранит проблемы с упорядочением и круговые зависимости.

Является ли время компиляции проблемой? Тогда...

Если время компиляции будет действительно проблемой, я бы рассмотрел либо:

  • Использование предварительно скомпилированных заголовков (это очень полезно для STL и BOOST)
  • Уменьшить связь через идиому PImpl, как описано в http://en.wikipedia.org/wiki/Opaque_pointer
  • Использовать общую общую компиляцию

Заключение

То, что вы делаете, не помещает все в заголовки.

В основном вы включаете все свои файлы в один и только один конечный источник.

Возможно, вы выигрываете в плане компиляции полного проекта.

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

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

Я бы потерял много времени, если бы мой проект был организован.

Ответ 2

Я не согласен с точкой 1.

Да, есть только один .cpp, а встроенное время с нуля - быстрее. Но вы редко строите с нуля. Вы вносите небольшие изменения, и ему нужно будет перекомпилировать весь проект каждый раз.

Я предпочитаю делать это наоборот:

  • сохранять общедоступные объявления в файлах .h.
  • сохранить определение для классов, которые используются только в одном месте в .cpp файлах

Итак, некоторые из моих .cpp файлов начинают выглядеть как Java или С# -код;)

Но, "поддержание элемента в .h" подходит, при разработке системы, из-за точки 2. вы сделали. Обычно я делаю это, пока строю иерархию классов, а позже, когда архитектура кода становится стабильной, я перемещаю код в файлы .cpp.

Ответ 3

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

Но...

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

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

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

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

PS: О С# или Java, вы должны иметь в виду, что эти языки не делают то, что вы говорите. Они фактически компилируют файлы независимо (например, файлы cpp) и хранят интерфейс по всему миру для каждого файла. Эти интерфейсы (и любые другие связанные интерфейсы) затем используются для связывания всего проекта, поэтому они могут обрабатывать циклические ссылки. Поскольку С++ выполняет только один компиляционный проход на файл, он не может глобально хранить интерфейсы. Поэтому вы должны писать их явно в файлах заголовков.

Ответ 4

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

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

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

Однако я разработал решение, в котором у меня есть объявление класса в файле .h, весь шаблон и встроенный код в файле .inl и вся реализация кода non template/inline в моем .cpp файле. Файл .inl # включен в нижней части моего .h файла. Это сохраняет чистоту и согласованность.

Ответ 5

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

Ответ 6

Возможно, вы захотите проверить Lazy С++. Он позволяет размещать все в одном файле, а затем выполняется до компиляции и разбивает код на файлы .h и .cpp. Это может предложить вам лучшее из обоих миров.

Медленное время компиляции обычно происходит из-за чрезмерной связи внутри системы, написанной на С++. Возможно, вам нужно разделить код на подсистемы с внешними интерфейсами. Эти модули могут быть скомпилированы в отдельных проектах. Таким образом, вы можете минимизировать зависимость между различными модулями системы.

Ответ 7

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

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

Ответ 8

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

С++ предназначен для h файлов, имеющих декларации, и cpp файлов, имеющих реализации. Компиляторы построены вокруг этого дизайна.

Да, люди обсуждают, хорошая ли архитектура, но это дизайн. Лучше тратить свое время на вашу проблему, чем изобретать новые способы разработки файловой архитектуры С++.

Ответ 9

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

Ответ 10

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

В любом случае не позволяйте времени компиляции определять, как вы структурируете свою программу. Лучше иметь программу, которая может быть легко читаемой и поддерживаемой по сравнению с той, которая скомпилируется за один 1,5 минуты вместо 2 минут.

Ответ 11

Я считаю, что, если вы не используете предварительно скомпилированные заголовки MSVC, и вы используете Makefile или другую систему сборки на основе зависимостей, при этом отдельные исходные файлы должны собираться быстрее при построении итеративно. Поскольку мое развитие почти всегда итеративно, я забочусь о том, как быстро он может перекомпилировать изменения, внесенные мной в файл x.cpp, чем в двадцати других исходных файлах, которые я не изменил. Кроме того, я делаю изменения гораздо чаще в исходных файлах, чем в API, поэтому они меняются реже.

Относительно круговых зависимостей. Я бы шагнул на шаг шаг вперед. У него было два класса, которые имели указатели друг на друга. Вместо этого я сталкиваюсь с ситуацией чаще, когда один класс требует другого класса. Когда это произойдет, я включаю заголовочный файл для зависимости в файле заголовка другого класса. Пример:

// foo.hpp
#ifndef __FOO_HPP__
#define __FOO_HPP__

struct foo
{
   int data ;
} ;

#endif // __FOO_HPP__

.

// bar.hpp
#ifndef __BAR_HPP__
#define __BAR_HPP__

#include "foo.hpp"

struct bar
{
   foo f ;
   void doSomethingWithFoo() ;
} ;
#endif // __BAR_HPP__

.

// bar.cpp
#include "bar.hpp"

void bar::doSomethingWithFoo()
{
  // Initialize f
  f.data = 0;
  // etc.
}

Причина, по которой я включаю это, немного не связанная с круговыми зависимостями, заключается в том, что я считаю, что есть альтернативы включению файлов заголовков волей-неволей. В этом примере исходный файл строки структуры не содержит файл заголовка struct foo. Это делается в файле заголовка. Это имеет преимущество в том, что разработчик, использующий панель, не должен знать о каких-либо других файлах, которые разработчик должен будет включить для использования этого файла заголовка.

Ответ 12

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

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

Таким образом, в интересах создания наиболее многоразового и поддерживаемого кода, возможно, только вставляйте встроенный и встроенный код в файлы заголовков.

Ответ 13

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

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

Все это, однако, не является проблемой при использовании библиотек, которые полностью содержатся в заголовках. "Это просто работает" для разумной хорошо написанной библиотеки.

Ответ 14

статические или глобальные переменные kludges еще менее прозрачны, возможно, не отлаживаются.

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

В MY kludged файлы, помещающие такие элементы в начало файла cpp, их легко найти.

"Возможно, это невозможно отлаживать", я имею в виду, что обычно я помещаю такой глобальный в окно WATCH. Так как он всегда в области видимости, окно WATCH всегда может получить к нему независимо от того, где встречается счетчик программ прямо сейчас. Поместив такие переменные за пределы {} в верхней части заголовочного файла, вы позволили бы всем нисходящим кодам "видеть" их. Поместив их INSIDE a {}, я бы подумал, что отладчик больше не будет рассматривать их как "в-сфере", если ваш счетчик программ находится за пределами {}. В то время как с kludge-global-at-Cpp-top, хотя он может быть глобальным до уровня, отображаемого в вашей link-map-pdb-etc, без внешнего оператора другие файлы Cpp не могут добраться до него, избегая случайного соединения.

Ответ 15

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

Ответ 16

Если вы используете классы шаблонов, вы все равно должны поместить всю реализацию в заголовок...

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

Ответ 17

Важная философия объектно-ориентированного программирования заключается в том, что скрытие данных ведет к инкапсулированным классам с реализацией, скрытой от пользователей. Это прежде всего для обеспечения уровня абстракции, где пользователи класса в основном используют общедоступные функции-члены для конкретных, а также статических типов. Затем разработчик класса может свободно изменять фактические реализации, если реализации не подвергаются пользователям. Даже если реализация является частной и объявлена ​​в заголовочном файле, изменение реализации потребует повторной компиляции всех зависимых кодовых баз. Принимая во внимание, что если реализация (определение функций-членов) находится в исходном коде (файл без заголовка), тогда библиотека изменяется, а зависимая кодовая база нуждается в повторной ссылке с пересмотренной версией библиотеки. Если эта библиотека динамически связана с одной, как с общей библиотекой, то сохранение подписи функции (интерфейса) одинаково, а реализация изменена не требует повторной привязки. Преимущество? Конечно.