0

Подскажите пожалуйста, какая разница между тем, что я напишу реализацию какого то условного класса или функции в одном хедер файле или разделю их на реализацию в .cpp и прототип в .h ?

Правильно я понимаю, что если мне нет необходимости потом компилировать код в библиотеку, то разницы между этими методами абсолютно нет ?

Optimus1
  • 1,045
  • Разница в том, что вынос реализации в .cpp создает новую единицу трансляции. – user7860670 Oct 03 '21 at 15:04
  • @user7860670 а хедер с реализацией тогда что? не единица трансляции? – ampawd Oct 03 '21 at 21:12
  • @ampawd Нет, хедер с реализацией - это по-прежнему хедер. – user7860670 Oct 03 '21 at 21:13
  • @user7860670 Тоесть по содержимому файла компилятор понимает что тут только реализация и создаёт единицу трансляции ? – ampawd Oct 03 '21 at 21:15
  • @ampawd Ничего такого он не понимает. Единица трансляции соответствует каждому файлу, явно указанному в аргументах командной строки компилятора. см https://ru.stackoverflow.com/questions/876163/c-%d0%a0%d0%b0%d0%b7%d0%bd%d0%b8%d1%86%d0%b0-%d0%bc%d0%b5%d0%b6%d0%b4%d1%83-%d0%b7%d0%b0%d0%b3%d0%be%d0%bb%d0%be%d0%b2%d0%be%d1%87%d0%bd%d1%8b%d0%bc-%d0%b8-%d1%84%d0%b0%d0%b9%d0%bb%d0%be%d0%bc-%d1%80%d0%b5%d0%b0%d0%bb%d0%b8%d0%b7%d0%b0%d1%86%d0%b8%d0%b8/876207#876207 – user7860670 Oct 03 '21 at 21:24

2 Answers2

4

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

Главный технический фактор: это можно сделать не всегда. Отделение объявлений от реализации - это важная часть того, как работает компиляция на C++. Если у вас есть циклическая зависимость в объявлениях, вы будете вынуждены иногда использовать forward-declaration, а иногда - полное описание класса. Вот как раз вариант с отдельными файлами для объявления и реализации дает решение этого вопроса - в файле .h вы работаете с forward-declaration, в файле .cpp - с полным объявлением.

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

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

Лично на мой взгляд, размер выходного файла и время сборки - это минорные факторы, а вот тот факт, что разработчик занят выверкой имен и разгребанием ошибок, вместо решения прикладной задачи - это главный шоустоппер такого решения. Ферма из тредрипперов под сборку обойдется дешевле времени разработчиков и реабилитации их после нудной работы по приведению хидеров в порядок.

Далее уже всякая лирика:

Технически, тела всего что засунуто в объявление класса, будут расцениваться компилятором как inline, следовательно он будет пытаться, по возможности, затолкать эти тела прямо на место, откуда они вызываются - в зависимости от настроек оптимизации, разумеется. Это может повлиять на скорость работы программы и на ее размер, причем в любую сторону.

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

Теперь кратко про человеческий фактор - это уже вопрос привычки и вкуса.

gbg
  • 22,253
3

Реализация всего в заголовочных файлах известна как концепция SCU. В качестве преимуществ (для крупных проектов) вы получаете

  • Радикальное сокращение времени сборки. Это основная причина для перехода на SCU. Когда у вас есть проект с n единицами трансляции, время сборки будет расти ~линейно при добавлении каждой новой единицы трансляции. В то же время при использовании SCU время сборки будет расти только ~логарифмически и добавление новых классов в проект практически никак не будет сказываться на времени сборки.
  • Сокращение памяти, требуемой для сборки. Дисковой и оперативной. Хотя одна "большая" единица трансляции будет очевидно занимать куда больше места, чем любая из "маленьких", общий размер маленьких единиц трансляции будет куда больше.
  • Выигрыш по оптимизации. Очевидно, что все автоматически становится inline.
  • Пропадает страх случайно запустить пересборку всего проекта с 0. Это особенно важно в контексте Непрерывной Интеграции, так как это то, чем постоянно занимается CI сервер.
  • В коде, в котором задействуется стадия динамической инициализации, исключается проблема Static initialization Order Fiasco, так как порядок инициализации в рамках одной единицы трансляции определен.
  • Сокращается риск нарушения One Definition Rule.

В качестве сложностей:

  • Разработчики должны четко соблюдать дисциплину организации заголовочных файлов. Header guards, единообразный порядок директив #include, обязательное обеспечение самодостаточности каждого заголовочного файла, правильный форвардинг, единство соглашения об именованиях и т.п. Проблема в том, что инструментов для соблюдения этого всего нет, а отход от установленных правил приводит к сообщениям об ошибках, которые еще более мутные, чем обычно.
  • Потенциальное увеличение числа файлов в проекте. Смотрите этот ответ.
  • Больше не получится сражаться на мечах, отмазываясь, что код компилируется.

некоторые числа:

user7860670
  • 29,796
  • 1
    Я правильно понял, что перекомпиляция, скажем, 100 строк из общих 10000 (находятся в 100 файлах) и линкинг 100 файлов требует существенно большего времени, чем компиляция всех 10000 строк и линкинг одного файла? Может у вас там были ("время сборки упало с ~14 минут до ~20 секунд") какие-то неучтенные проблемы с make? – avp Oct 03 '21 at 19:15
  • 1
    @avp Нет, тут везде идет речь о полной сборке. Инкрементальная сборка примерно сравнима по скорости будет. Однако я вполне допускаю, что в некоторых сценариях за счет долгой линковки инкрементальная сборка запросто может оказаться несколько медленнее. – user7860670 Oct 03 '21 at 19:51
  • 1
    Понял. Может все же упомянуть в ответе, 14 сек и 20 мин это полная сборка? – avp Oct 03 '21 at 19:56
  • 1
    @avp Упомянул, и еще про одно ядро тоже. – user7860670 Oct 03 '21 at 20:09