Несколько классов в файле заголовка против одного файла заголовка для каждого класса

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

Each class shall have it own header and implementation file.

Итак, если мы написали класс под названием MyString, нам понадобится связанный MyStringh.h и MyString.cxx.

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

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

Ответ 1

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

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

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

Ответ 2

Переполнено тысячами строк кода?

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Ответ 3

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

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

Ответ 4

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

Ответ 5

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

Ответ 6

G'day,

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

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

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

Вам также может понравиться прочитать Code Complete от Steve McConnell за отличную главу о руководящих принципах кодирования. Фактически, эта книга отлично читала, что я постоянно возвращаюсь к

веселит, Rob

Ответ 7

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

Ответ 8

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

Ответ 10

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

Ответ 11

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

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

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

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

Ответ 12

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

  • Деревья наследования
  • Классы, которые используются только в пределах очень.
  • Некоторые утилиты просто помещаются в общий 'utils.h'