Вы можете структурировать программу на C++ так, чтобы (почти) весь код находился в файлах заголовков. По сути, это похоже на программу на C# или Java. Однако вам понадобится хотя бы один файл .cpp для извлечения всех файлов заголовков при компиляции. Теперь я знаю, что некоторым людям эта идея совершенно не понравится. Но я не нашел в этом убедительных недостатков. Могу перечислить некоторые преимущества:
[1] Более быстрое время компиляции. Все файлы заголовков анализируются только один раз, потому что существует только один файл .cpp. Кроме того, один файл заголовка не может быть включен более одного раза, иначе вы получите прерывание сборки. Есть и другие способы достижения более быстрой компиляции при использовании альтернативного подхода, но это так просто.
[2] Он избегает циклических зависимостей, делая их абсолютно ясными. Если ClassA в ClassA.h имеет круговую зависимость от ClassB в ClassB.h, я должен указать прямую ссылку, и она выпирает. (Обратите внимание, что это не похоже на C# и Java, где компилятор автоматически разрешает циклические зависимости. Это поощряет плохие методы кодирования IMO). Опять же, вы можете избежать циклических зависимостей, если ваш код был в файлах .cpp, но в реальном проекте файлы .cpp, как правило, включают случайные заголовки, пока вы не сможете понять, кто от кого зависит.
Твои мысли?
+1 Хороший вопрос, ведь такая практика программирования используется даже в некоторых крупных проектах. (например, www.ogre3D.com)
Пункт 1 игнорирует перспективу использования более одного ядра / машины для компиляции вашего проекта. Распространение компиляции нескольких файлов cpp по нескольким ядрам может превзойти компиляцию программы, поскольку один файл cpp компилируется только на одном ядре.
А как насчет производительности? Разве компилятор не может улучшить оптимизацию, если видит весь код за один раз? (например, встраивание и т. д.)





Для меня очевидным недостатком является то, что вам всегда нужно создавать весь код сразу. С файлами .cpp у вас может быть отдельная компиляция, поэтому вы перестраиваете только те биты, которые действительно изменились.
Ах хороший улов. Таким образом, не будет никаких «инкрементных» сборок, и в этом смысле время компиляции будет медленнее. Однако на практике я нахожу сборку довольно быстрой, поскольку мой код разбит на отдельные статические библиотеки.
Это ничего не изменит. Если у вас есть весь код в файле .h, а затем вы вносите изменения - все .cpp и .h (технически: .cpp), которые включают его, должны быть перестроены.
@Nelson: Именно поэтому я не согласен с тем, чтобы весь код был в файлах .h.
Я не согласен с пунктом 1.
Да, есть только один .cpp, и время сборки с нуля происходит быстрее. Но вы редко строите с нуля. Вы вносите небольшие изменения, и каждый раз придется перекомпилировать весь проект.
Я предпочитаю делать наоборот:
Итак, некоторые из моих файлов .cpp начинают выглядеть как код Java или C#;)
Но подход 'хранить вещи в .h' хорош при проектировании системы из-за пункта 2., который вы сделали. Обычно я делаю это, когда строю иерархию классов, а позже, когда архитектура кода становится стабильной, я перемещаю код в файлы .cpp.
На самом деле разногласий недостаточно. Это не подлежит обсуждению: пункт 1 категорически неверен по причинам, о которых вы упомянули.
@Konrad: можно утверждать, что наличие компилятора, который поддерживает предварительно скомпилированные заголовки (MSVC и GCC), на самом деле не перестраивает все файлы заголовков - только взаимозависимые - точно так же, как подход с файлом .cpp. Однако настройка этого означала бы наличие отдельного файла PCH для каждого файла .h.
Кладжи статических или глобальных переменных еще менее прозрачны, возможно, не поддаются отладке.
например, подсчет общего количества итераций для анализа.
В MY kludged файлах размещение таких элементов в верхней части файла cpp упрощает их поиск.
Под «возможно, не отлаживаемым» я подразумеваю, что обычно я помещаю такой глобальный объект в окно WATCH. Поскольку он всегда находится в области видимости, окно WATCH всегда может добраться до него, независимо от того, где сейчас находится счетчик программы. Помещая такие переменные вне {} в верхней части файла заголовка, вы позволяете всему нижестоящему коду "видеть" их. Помещая их ВНУТРИ {}, я бы сразу подумал, что отладчик больше не будет рассматривать их как "входящие в область видимости", если ваш счетчик программ находится за пределами {}. Принимая во внимание, что с kludge-global-at-Cpp-top, даже если он может быть глобальным в той степени, в которой он отображается в вашей link-map-pdb-etc, без extern-выражения другие файлы Cpp не могут до него добраться. , избегая случайного сцепления.
Вы неправильно понимаете, как предполагалось использовать этот язык. Файлы .cpp действительно (или должны быть, за исключением встроенного кода и кода шаблона) единственные модули исполняемого кода, которые есть в вашей системе. Файлы .cpp компилируются в объектные файлы, которые затем связываются вместе. Файлы .h существуют исключительно для предварительного объявления кода, реализованного в файлах .cpp.
Это приводит к сокращению времени компиляции и уменьшению размера исполняемого файла. Он также выглядит значительно чище, потому что вы можете быстро получить представление о своем классе, посмотрев на его объявление .h.
Что касается встроенного кода и кода шаблона, поскольку оба они используются для генерации кода компилятором, а не компоновщиком, они всегда должны быть доступны компилятору для каждого файла .cpp. Поэтому единственное решение - включить его в ваш .h файл.
Однако я разработал решение, в котором у меня есть объявление моего класса в файле .h, весь шаблон и встроенный код в файле .inl и вся реализация нешаблонного / встроенного кода в моем файле .cpp. Файл .inl # включен в конец моего файла .h. Это сохраняет вещи чистыми и последовательными.
На мой взгляд, .inl (или .tcc, в GNU libstdC++) - это лучшая практика. +1!
Если бы люди делали вещи только так, как предполагалось для использования языка, не было бы метапрограммирования шаблонов. Ой, подождите, я не уверен, что это будет плохо ;-)
Вы правильно говорите, что ваше решение работает. Он может даже не иметь минусов для вашего текущего проекта и среды разработки.
Но...
Как утверждали другие, размещение всего вашего кода в файлах заголовков вызывает полную компиляцию каждый раз, когда вы меняете одну строку кода. Возможно, это еще не проблема, но ваш проект может вырасти до того момента, когда время компиляции станет проблемой.
Другая проблема - при совместном использовании кода. Возможно, вас это еще не касается, но важно, чтобы как можно больше кода было скрыто от потенциального пользователя вашего кода. Помещая свой код в файл заголовка, любой программист, использующий ваш код, должен просмотреть весь код, а там просто интересно, как его использовать. Помещение вашего кода в файл cpp позволяет доставлять только двоичный компонент (статическую или динамическую библиотеку) и его интерфейс в виде файлов заголовков, что может быть проще в некоторых средах.
Это проблема, если вы хотите иметь возможность превратить текущий код в динамическую библиотеку. Поскольку у вас нет надлежащего объявления интерфейса, отделенного от фактического кода, вы не сможете доставить скомпилированную динамическую библиотеку и ее интерфейс использования в виде читаемых файлов заголовков.
Возможно, у вас еще нет этих проблем, поэтому я говорил, что ваше решение может подойти в вашей текущей среде. Но всегда лучше быть готовым к любым изменениям, и некоторые из этих вопросов следует решать.
PS: Что касается C# или Java, вы должны иметь в виду, что эти языки не делают то, что вы говорите. Фактически они компилируют файлы независимо (например, файлы cpp) и хранят интерфейс глобально для каждого файла. Эти интерфейсы (и любые другие связанные интерфейсы) затем используются для связывания всего проекта, поэтому они могут обрабатывать циклические ссылки. Поскольку C++ выполняет только один проход компиляции для каждого файла, он не может хранить интерфейсы глобально. Вот почему вы должны явно писать их в файлах заголовков.
Возможно, вы захотите проверить Ленивый C++. Он позволяет вам поместить все в один файл, а затем он запускается перед компиляцией и разбивает код на файлы .h и .cpp. Это может предложить вам лучшее из обоих миров.
Медленное время компиляции обычно происходит из-за чрезмерного связывания в системе, написанной на C++. Возможно, вам нужно разбить код на подсистемы с внешними интерфейсами. Эти модули могут быть скомпилированы в отдельные проекты. Таким образом можно свести к минимуму зависимость между различными модулями системы.
Мне нравится думать о разделении файлов .h и .cpp с точки зрения интерфейсов и реализаций. Файлы .h содержат описания интерфейсов еще одного класса, а файлы .cpp содержат их реализации. Иногда возникают практические проблемы или ясность, которые мешают полностью чистому разделению, но я начинаю именно с этого. Например, небольшие функции доступа я обычно кодирую в объявлении класса для ясности. Более крупные функции закодированы в файле .cpp.
В любом случае не позволяйте времени компиляции определять, как вы будете структурировать свою программу. Лучше иметь программу, которую можно читать и поддерживать, а не ту, которая компилируется за 1,5 минуты вместо 2 минут.
Не в моих проектах: исходные файлы (CPP) включают только нужные им заголовки (HPP). Поэтому, когда мне нужно перекомпилировать только один CPP из-за небольшого изменения, у меня в десять раз больше файлов, которые не перекомпилируются.
Возможно, вам следует разбить свой проект на более логичные источники / заголовки: модификация в реализации класса A НЕ должна нуждаться в перекомпиляции реализаций класса B, C, D, E и т. д.
Циклические зависимости в коде?
Извините, но у меня еще не было такой проблемы, чтобы быть реальной проблемой: скажем, 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. */ }
Таким образом, нет проблем с зависимостями и по-прежнему быстрое время компиляции.
Я что-то пропустил?
in a real-world project, cpp files tend to include random headers until you can't figure out who depends on whom
Конечно. Но затем, если у вас есть время реорганизовать эти файлы для создания решения «one CPP», тогда у вас будет время очистить эти заголовки. Мои правила для заголовков:
В любом случае все заголовки должны быть самодостаточными, что означает:
Это устранит проблемы с упорядочением и циклические зависимости.
Если время компиляции действительно является проблемой, я бы рассмотрел либо:
То, что вы делаете, не помещает все в заголовки.
По сути, вы включаете все свои файлы в один и только один окончательный источник.
Возможно, вы выигрываете с точки зрения компиляции полного проекта.
Но при компиляции для одного небольшого изменения вы всегда проиграете.
Я знаю, что при кодировании я часто компилирую небольшие изменения (хотя бы для того, чтобы компилятор проверял мой код), а затем в последний раз полностью изменяю проект.
Я бы потерял много времени, если бы мой проект был организован по-вашему.
Вы отказываетесь от одной вещи, без которой мне было бы трудно жить, - это анонимные пространства имен.
Я считаю, что они невероятно ценны для определения служебных функций, специфичных для класса, которые должны быть невидимы за пределами файла реализации класса. Они также отлично подходят для хранения любых глобальных данных, которые должны быть невидимы для остальной части системы, например, одноэлементного экземпляра.
Вы выходите за рамки дизайна языка. Хотя у вас могут быть некоторые преимущества, в конечном итоге он укусит вас под зад.
C++ разработан для файлов h, имеющих объявления, и файлов cpp, имеющих реализации. Компиляторы построены вокруг этой конструкции.
да, люди спорят, хорошая ли это архитектура, но дело в дизайне. Лучше потратить время на решение своей проблемы, чем изобретать новые способы проектирования файловой архитектуры C++.
Что ж, как многие отмечали, у этой идеи есть много минусов, но чтобы немного сбалансировать и предоставить преимущества, я бы сказал, что наличие некоторого библиотечного кода полностью в заголовках имеет смысл, поскольку это сделает его независимым от других. настройки в проекте, в котором он используется.
Например, если кто-то пытается использовать разные библиотеки с открытым исходным кодом, они могут быть настроены на использование разных подходов для связывания с вашей программой - некоторые могут использовать динамически загружаемый библиотечный код операционной системы, другие настроены на статическое связывание; некоторые могут быть настроены на использование многопоточности, а другие - нет. И для программиста вполне может оказаться непосильной задачей - особенно с ограниченным временем - попытаться отсортировать эти несовместимые подходы.
Однако все это не является проблемой при использовании библиотек, которые полностью содержатся в заголовках. «Это просто работает» для разумной, хорошо написанной библиотеки.
Я считаю, что если вы не используете предварительно скомпилированные заголовки 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 bar не включает файл заголовка struct foo. Это делается в заголовочном файле. Это имеет преимущество в том, что разработчик, использующий bar, не должен знать ни о каких других файлах, которые разработчик должен был бы включить для использования этого файла заголовка.
Одна из проблем с кодом в заголовках заключается в том, что он должен быть встроен, иначе у вас будут проблемы с несколькими определениями при связывании нескольких единиц перевода, которые включают тот же заголовок.
В исходном вопросе указывалось, что в проекте всегда был только один cpp, но это не тот случай, если вы создаете компонент, предназначенный для многоразовой библиотеки.
Поэтому в интересах создания максимально многоразового и поддерживаемого кода помещайте только встроенный и встраиваемый код в файлы заголовков.
Один из недостатков вашего подхода - вы не можете выполнять параллельную компиляцию. Вы можете подумать, что теперь компилируете быстрее, но если у вас есть несколько файлов .cpp, вы можете создавать их параллельно либо на нескольких ядрах на вашем собственном компьютере, либо с помощью распределенной системы сборки, такой как distcc или Incredibuild.
Никто не упомянул, что для компиляции больших файлов требуется много памяти. Для одновременной компиляции всего проекта потребуется такой огромный объем памяти, что это просто невозможно, даже если бы вы могли поместить весь код в заголовки.
Если вы используете классы шаблонов, вы все равно должны поместить всю реализацию в заголовок ...
Компиляция всего проекта за один раз (с помощью одного базового файла .cpp) должна позволить что-то вроде «Оптимизация всей программы» или «Межмодульная оптимизация», которая доступна только в нескольких продвинутых компиляторах. Это невозможно со стандартным компилятором, если вы предварительно компилируете все свои файлы .cpp в объектные файлы, а затем связываете их.
Важная философия объектно-ориентированного программирования заключается в том, что скрытие данных ведет к инкапсулированным классам, реализация которых скрыта от пользователей. Это в первую очередь для обеспечения уровня абстракции, на котором пользователи класса в основном используют общедоступные функции-члены, например, специфичные для экземпляра, а также статические типы. Затем разработчик класса может изменять фактические реализации, при условии, что реализации не доступны пользователям. Даже если реализация является частной и объявлена в файле заголовка, изменение реализации потребует повторной компиляции всей зависимой кодовой базы. Принимая во внимание, что если реализация (определение функций-членов) находится в исходном коде (файл без заголовка), тогда библиотека изменяется, и зависимая кодовая база должна повторно связываться с исправленной версией библиотеки. Если эта библиотека динамически связана как общая библиотека, то сохранение сигнатуры функции (интерфейса) такой же и изменение реализации также не требует повторного связывания. Преимущество? Конечно.
Если вы сделаете политику максимально возможной, используйте форвардные объявления. Мало того, что проект, состоящий из большого количества файлов .cpp, будет компилироваться быстрее, вам почти никогда не придется беспокоиться о циклических зависимостях. В принципе, если вы не необходимость полное определение в заголовке, используйте прямое объявление.