12

Стоит ли определять функции сразу в заголовочном файле .h, или всегда определять в файле .cpp?

Regent
  • 19,134

5 Answers5

14

Если функция будет использоваться только внутри одного CPP, то выносить в заголовочный файл большого смысла нет. Если функции вызывают друг друга, то можно разделить объявление и реализацию. Типа инкапсуляция, "приватная функция".

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

Если функция шаблонная (template) или инлайновая (inline), то без вариантов следует размещать и объявление, и реализацию в заголовочном файле, потому что шаблонные функции по сути текстовые шаблоны, и их компилятор не сможет "извлечь" из CPP.


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

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


В целом, вопрос сводится к "делать ли функцию инлайновой". А вот делать ли её инлайновой — это уже вопрос производительности выполнения против возможности перелинковки против скорости сборки.

См. также When should I write the keyword 'inline' for a function/method?

Kyubey
  • 32,103
  • 2
    inline собственно и означает «функция размещена в header'е, компоновщику заткнуться» (и не означает подстановку в точку вызова). – VladD Jun 14 '15 at 14:41
  • @VladD Пруфлинк? А то я не языковой юрист. – Kyubey Jun 14 '15 at 14:48
  • Ссылка на обсуждение на en.SO покатит? – VladD Jun 14 '15 at 14:48
  • @VladD Если с тегом language-lawyer, то да. :) – Kyubey Jun 14 '15 at 14:49
  • пока нашёл вот и вот. – VladD Jun 14 '15 at 14:52
  • @VladD Первая ссылка хорошая. Но мне было бы интересно увидеть цитату из стандарта. В стандарте роли компилятора и линковщика настолько явно разделены? – Kyubey Jun 14 '15 at 15:01
  • Ну и стандарт, 7.2.1/2: An implementation is not required to perform this inline substitution at the point of call, 7.2.1/4: An inline function shall be defined in every translation unit in which it is odr-used and shall have exactly the same definition in every case [...] An inline function with external linkage shall have the same address in all translation units. – VladD Jun 14 '15 at 15:01
  • "Если объявление и определение инлайновой функции не разместить в заголовочном файле, то на стадии сборки посыпятся ошибки, что "определение функции не найдено". - в общем случае да, но не обязательно так. Если в какой-то единице трансляции понадобился неинлайновый вызов инлайновой функции, то для нее в соответствующем объектном файле будет сгенерировано обычное тело. Это обычное тело потом будет подхвачено линкером и использовано из всех остальных единицах трансляции. Ошибок линкера не будет. – AnT stands with Russia Jul 19 '17 at 23:46
10

Формально вы можете класть реализацию функции в header. В некоторых случаях (например, шаблон) это даже необходимо¹. Но тем не менее, имеет смысл предпочитать класть функцию в .cpp-файл.

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

Следующая причина: инкапсуляция. Если вы положили функцию в header, то она видна всем. А значит, другие пользователи будут видеть функцию (и, возможно, писать свой код ориентируясь не на документацию, а на конкретную имплементацию). Пользователь, конечно, может заглянуть и в .cpp-файл, но если вы поставляете библиотеку, вы можете отдать лишь header + скомпилированный объектный файл, так что заглянуть будет некуда!

Затем, ещё одна проблема есть для функций-членов. Допустим, у вас есть класс A с публичной функцией-членом f, и класс B с публичной функцией-членом g. Пусть реализация класса A пользуется B::g, а реализация класса B — функцией A::f. Пусть реализация класса A лежит в header-файле a.h, а класса B — в b.h. Поскольку A пользуется B::g, то a.h должен включать b.h. По аналогичной причине b.h должен включать a.h. Таким образом, если первым при компиляции какого-нибудь cpp-файла подключается a.h, то в нём подключается b.h, который теперь не может рекурсивно подключить a.h.

Проблема с взаимно-рекурсивными функциями-членами решилась бы, если бы в header-файлах были только декларации классов, и вместо включения b.h можно было бы просто написать class B;².


Ещё один случай, упомянутый в других ответах — локальная функция внутри .cpp-файла, которая нужна только внутри данного .cpp и не экспортируется. В этом случае, однако, вам нужно поместить её в анонимный namespace, чтобы избежать неприятных сюрпризов от компоновщика. Подумайте, что будет, если вы определите нечаянно две «невидимые» функции с одинаковыми сигнатурами (и ужаснитесь).


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

² Поскольку форвард-декларации функций работают на уровне отдельной функции, эта проблема, кажется, не возникает для свободных функций (не-членов).

VladD
  • 206,799
  • 2
    Помимо анонимного пространства имен подходит и добавление static к такой функции. – αλεχολυτ Jul 20 '17 at 07:34
  • 1
    @alexolut: Это да, но это вроде более сишный метод. Оба работают, конечно. – VladD Jul 20 '17 at 09:51
4

Есть два правила:

  • Функция должна быть объявлена (declared) до ее использования
  • Функция должна быть определена (defined) ровно один раз

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

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

Если какая-то функция не объявлена, то компилятор выдаст вам ошибку при попытке использовать ее в другом файле исходника.

Aleksei Averchenko
  • 1,049
  • 6
  • 12
3

Лучше всегда в .cpp (кроме шаблонных методов, разумеется)

  • 1
    На чём основано это утверждение? – Regent Jun 14 '15 at 14:13
  • На том, как меня учили и что я чаще вижу в чужом коде:) А вообще, даже из названий, по-моему, это следует: .h = Header, .cpp - Source. + Распространять и использовать свои DLL намного удобнее, если в Header у Вас вынесены прототипы и этот файл подключается к пользователю Вашей библиотеки. Можно еще привести "ЗА" – Алексей Саровский Jun 14 '15 at 14:17
  • Да, я тут подумал, что меня могут неправильно понять: я имел ввиду, что прототип функции в hpp, а реализация в срр – Алексей Саровский Jun 14 '15 at 14:24
  • Меня, на самом деле, до сих пор интересует, не является ли этот вопрос "primarily opinion-based". Поэтому хотелось бы увидеть объективные факты/доказательства тех или иных утверждений. Сам я из С++ мало что помню. – Regent Jun 14 '15 at 14:29
0

На самом деле разницы нет, главное чтобы функция определялась до её использования. Я на практике встречал часто, что в хедере определяется функция, а в срр уже описывается. Если функция будет описана после её вызова (по коду)то она сработает только в том случае если её определили заранее допустим в хедере.

ActivX
  • 833