Несколько классов в файле заголовка против одного файла заголовка на класс - PullRequest
67 голосов
/ 26 августа 2008

По какой-то причине у нашей компании есть руководство по кодированию, которое гласит:

Each class shall have it's own header and implementation file.

Поэтому, если бы мы написали класс с именем MyString, нам потребовалось бы связать MyStringh.h и MyString.cxx .

Кто-нибудь еще делает это? Кто-нибудь видел какие-либо последствия для производительности компиляции в результате? 5000 классов в 10000 файлах компилируются так же быстро, как 5000 классов в 2500 файлах? Если нет, то заметна ли разница?

[Мы кодируем C ++ и используем GCC 3.4.4 в качестве нашего повседневного компилятора]

Ответы [ 13 ]

71 голосов
/ 26 августа 2008

Термин здесь единица перевода , и вы действительно хотите (если возможно) иметь один класс на единицу перевода, т.е. одну реализацию класса на файл .cpp, с соответствующим файлом .h с тем же именем .

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

Также вы обнаружите, что о многих ошибках / диагностике сообщается через имя файла («Ошибка в Myclass.cpp, строка 22»), и это помогает, если между файлами и классами существует взаимно-однозначное соответствие. (Или, я полагаю, вы могли бы назвать это перепиской 2 к 1).

62 голосов
/ 20 сентября 2008

Разбиты тысячи строк кода?

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

Но, поиграв с источниками, следуя философии «давайте соединим все», мы пришли к выводу, что только тот, кто написал файл, надеется, что его не потеряют внутри. Даже с IDE легко пропустить вещи, потому что когда вы играете с источником в 20000 строк, вы просто закрываете свой разум для всего, что не относится к вашей проблеме.

Пример из реальной жизни: иерархия классов, определенная в этих тысячах исходных строк, замкнулась на алмазное наследование, а некоторые методы были переопределены в дочерних классах методами с точно таким же кодом. Это было легко упущено (кто хочет исследовать / проверить исходный код на 20 000 строк?), И когда оригинальный метод был изменен (исправление ошибок), эффект был не таким универсальным, как исключение.

Зависимости становятся круглыми?

У меня была эта проблема с шаблонным кодом, но я видел похожие проблемы с обычным кодом C ++ и C.

Разделение ваших источников на 1 заголовок для структуры / класса позволяет:

  • Ускорение компиляции, поскольку вы можете использовать прямое объявление символа вместо включения целых объектов
  • Имеют циклические зависимости между классами (§) (то есть класс A имеет указатель на B, а B имеет указатель на A)

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

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

Для моей шаблонной программы мне пришлось разделить мои заголовки на два файла: файл .HPP, содержащий объявление / определение класса шаблона, и файл .INL, содержащий определения указанных методов класса.

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

И затем, если кому-то понадобится только небольшая часть кода с решением с одним заголовком, ему все равно придется платить за более медленную компиляцию.

(§) Обратите внимание, что вы можете иметь циклические зависимости между классами, если знаете, какой класс кому принадлежит. Это обсуждение классов, которые знают о существовании других классов, а не круговых зависимостей shared_ptr antipattern.

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

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

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

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

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

Вопрос о круговых зависимостях

underscore-d спрашивает:

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

underscore_d, 13 ноября в 23:20

Допустим, у вас есть 2 шаблона классов, A и B.

Допустим, определение класса A (соответственно B) имеет указатель на B (соответственно A). Скажем также, что методы класса A (соответственно B) фактически вызывают методы из B (соответственно A).

У вас круговая зависимость как в определении классов, так и в реализации их методов.

Если бы A и B были обычными классами, а методы A и B были в файлах .CPP, проблем не было бы: вы использовали бы предварительное объявление, имели бы заголовок для каждого определения класса, тогда каждый CPP включал бы оба HPP .

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

Это означает:

  1. заголовок определения A.def.hpp и B.def.hpp
  2. заголовок реализации A.inl.hpp и B.inl.hpp
  3. для удобства "наивный" заголовок A.hpp и B.hpp

Каждый заголовок будет иметь следующие черты:

  1. В A.def.hpp (соответственно B.def.hpp) у вас есть предварительное объявление класса B (соответственно A), которое позволит вам объявить указатель / ссылку на этот класс
  2. A.inl.hpp (соответственно B.inl.hpp) будет включать в себя как A.def.hpp, так и B.def.hpp, что позволит методам из A (соответственно B) использовать класс B (соответственно . А).
  3. A.hpp (соответственно B.hpp) будет напрямую включать в себя как A.def.hpp, так и A.inl.hpp (соответственно B.def.hpp и B.inl.hpp)
  4. Конечно, все заголовки должны быть самодостаточными и защищены защитниками заголовков

Наивный пользователь будет включать A.hpp и / или B.hpp, таким образом игнорируя весь беспорядок.

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

Обратите внимание, что это был крайний случай (два шаблона знают друг друга). Я ожидаю, что большинству кода не нужен этот трюк.

10 голосов
/ 26 августа 2008

Мы делаем это на работе, проще найти материал, если класс и файлы имеют одинаковые имена. Что касается производительности, у вас действительно не должно быть 5000 классов в одном проекте. Если вы это сделаете, возможно, будет проведен некоторый рефакторинг.

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

8 голосов
/ 26 августа 2008

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

7 голосов
/ 26 августа 2008

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

5 голосов
/ 26 августа 2008

Большинство мест, где я работал, следовали этой практике. Я действительно написал стандарты кодирования для BAE (Aust.) Вместе с причинами, по которым вместо того, чтобы просто вырезать что-то на камне без реального оправдания.

Что касается вашего вопроса об исходных файлах, то не так много времени для компиляции, а скорее проблема в том, чтобы найти соответствующий фрагмент кода. Не все используют IDE. И зная, что вы просто ищете MyClass.h, а MyClass.cpp действительно экономит время по сравнению с запуском «grep MyClass *. (H | cpp)» над кучей файлов и последующей фильтрацией операторов #include MyClass.h ...

Помните, что существуют обходные пути для воздействия большого количества исходных файлов на время компиляции. См. Big Scale C ++ Software Design от John Lakos для интересного обсуждения.

Возможно, вам также захочется прочитать Code Complete от Стива Макконнелла (Steve McConnell), где вы найдете отличную главу о правилах кодирования. На самом деле, эта книга - отличное чтение, к которому я постоянно возвращаюсь

3 голосов
/ 27 августа 2008

Лучшая практика, как уже говорили другие, состоит в том, чтобы поместить каждый класс в отдельную единицу перевода с точки зрения обслуживания и понятности кода. Однако в крупных системах это иногда не рекомендуется - см. Раздел «Сделайте эти исходные файлы больше» в статье этой статьи Брюса Доусона для обсуждения компромиссов.

3 голосов
/ 26 августа 2008

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

2 голосов
/ 02 декабря 2010

Я нашел эти рекомендации особенно полезными, когда дело доходит до заголовочных файлов: http://google -styleguide.googlecode.com / SVN / багажник / cppguide.xml # Header_Files

1 голос
/ 07 января 2015

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

Я лично группирую связанные классы в один файл, а затем даю такому имени осмысленное имя, которое не нужно будет менять, даже если имя класса изменится. Наличие меньшего количества файлов также облегчает прокрутку файлового дерева. Я использую Visual Studio в Windows и Eclipse CDT в Linux, и у обоих есть сочетания клавиш, которые сразу переходят к объявлению класса, поэтому найти объявление класса легко и быстро.

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

Выбор также зависит от типа проекта, над которым он работает. По моему мнению, проблема не заслуживает черно-белого ответа, поскольку у любого выбора есть свои плюсы и минусы.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...