Плюсы и минусы размещения всего кода в заголовочных файлах на C ++? - PullRequest
45 голосов
/ 11 октября 2008

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

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

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

Ваши мысли?

Ответы [ 17 ]

34 голосов
/ 11 октября 2008

Причина [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. */ }

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

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

При работе над "реальными" проектами

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

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

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

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

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

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

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

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

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

Заключение

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

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

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

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

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

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

26 голосов
/ 11 октября 2008

Я не согласен с пунктом 1.

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

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

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

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

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

17 голосов
/ 11 октября 2008

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

Но ...

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

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

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

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

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

11 голосов
/ 11 октября 2008

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

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

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

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

9 голосов
/ 11 октября 2008

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

3 голосов
/ 11 октября 2008

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

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

3 голосов
/ 11 октября 2008

Одна вещь, от которой вы отказываетесь, без которой мне было бы трудно жить, - это анонимные пространства имен.

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

3 голосов
/ 11 октября 2008

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

C ++ предназначен для h-файлов с объявлениями и cpp-файлов с реализациями. Компиляторы построены вокруг этого дизайна.

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

3 голосов
/ 12 октября 2008

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

2 голосов
/ 12 октября 2008

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

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

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

...