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

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

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 в качестве нашего повседневного компилятора]

12 ответов

Решение

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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 (соответственно A),
  3. A.hpp (соответственно B.hpp) будет напрямую включать в себя как A.def.hpp, так и A.inl.hpp (соответственно B.def.hpp и B.inl.hpp)
  4. Конечно, все заголовки должны быть самодостаточными и защищены защитой заголовков.

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

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

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

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

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

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

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

Большинство мест, где я работал, следовали этой практике. Я фактически написал стандарты кодирования для 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), где вы найдете отличную главу о правилах кодирования. На самом деле, эта книга - отличное чтение, к которому я постоянно возвращаюсь

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

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

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

Два слова: бритва Оккама. Храните один класс на файл с соответствующим заголовком в отдельном файле. Если вы делаете иначе, например, сохраняя часть функциональности для каждого файла, тогда вам нужно создать все виды правил о том, что составляет часть функциональности. Можно получить гораздо больше, сохраняя один класс на файл. И даже наполовину достойные инструменты могут обрабатывать большое количество файлов. Проще говоря, мой друг.

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

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

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

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

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

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

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

Другие вопросы по тегам