Каковы преимущества и недостатки разделения объявления и определения, как в C++?

в C++ объявление и определение функций, переменных и констант можно разделить следующим образом:

function someFunc();

function someFunc()
{
  //Implementation.
}

на самом деле, в определении классов это часто бывает. Класс обычно объявляется с его членами в a .H файл, и они затем определяются в соответствующем .C файл.

каковы преимущества и недостатки такого подхода?

11 ответов


исторически это должно было помочь компилятору. Вы должны были дать ему список имен, прежде чем он их использовал - было ли это фактическое использование или прямое объявление (прототип funcion по умолчанию C).

современные компиляторы для современных языков показывают, что это больше не является необходимостью, поэтому синтаксис C & C++(а также Objective-C и, возможно, другие) здесь является историческим багажом. На самом деле это одна из самых больших проблем с C++, что даже добавление соответствующего модуля система не решит.

Недостатки: много сильно вложенных файлов include (я прослеживал деревья include раньше, они удивительно огромны) и избыточность между объявлением и определением - все это приводит к более длительному времени кодирования и более длительному времени компиляции (когда-либо сравнивали время компиляции между сопоставимыми проектами C++ и c#? Это одна из причин разницы). Файлы заголовков должны предоставляться пользователям любых предоставляемых компонентов. Вероятность нарушений ОРВ. Опора на предусилитель-процессор (многие современные языки не нужны препроцессор шаг), которая делает ваш код более хрупким и более трудным для инструментов для разбора.

плюсы: не сильно. Вы можете утверждать, что вы получаете список имен функций, сгруппированных в одном месте для целей документации, но большинство IDE имеют какую - то способность сворачивания кода в эти дни, и проекты любого размера должны использовать генераторы doc (такие как doxygen) в любом случае. С более чистым, pre-processor-менее, синтаксис основанный модулем инструментам проще следовать вашему коду и предоставлять это и многое другое, поэтому я думаю, что это "преимущество" является спорным.


это артефакт того, как работают компиляторы C/C++.

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

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

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

void foo()
{
  bar();
}

void bar()
{
  foo();
}

здесь foo не будет компилироваться как bar - это неизвестно. Если вы переключите две функции вокруг,bar не будет компилироваться как foo неизвестно.

если вы раздельное объявление и определение, хотя, вы можете заказать функции, как вы хотите:

void foo();
void bar();

void foo()
{
  bar();
}

void bar()
{
  foo();
}

здесь, когда компилятор обрабатывает foo он уже знает подпись функция называется bar, и счастлив.

конечно, компиляторы могут работать по-другому, но именно так они работают на C, C++ и в некоторой степени Objective-C.

недостатки:

ничего напрямую. Если вы все равно используете C/C++, это лучший способ сделать что-то. Если у вас есть выбор языка/компилятора, возможно, вы можете выбрать тот, где это не проблема. Единственное, что нужно учитывать при разделении объявлений на заголовочные файлы, - это избегать взаимно рекурсивные # include-операторы-но это то, для чего предназначены охранники.

плюсы:

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

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


существует два основных преимущества разделения объявления и определения на заголовочные и исходные файлы C++. Во-первых, вы избегаете проблем с Одно Правило Определения когда ваш класс / функции / что угодно #included более чем в одном месте. Во-вторых, делая вещи таким образом, вы отделяете интерфейс и реализацию. Пользователям вашего класса или библиотеки нужно только увидеть файл заголовка, чтобы написать код, который его использует. Вы также можете сделать этот шаг дальше с Идиому Pimpl и сделайте так, чтобы код пользователя не приходилось перекомпилировать каждый раз, когда изменяется реализация библиотеки.

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

другим недостатком на практике является то, что для записи (и отладки!) хороший код, который использует стороннюю библиотеку, вы обычно должны видеть внутри нее. Это означает доступ к исходному коду, даже если вы не можете его изменить. Если у вас есть только файл заголовка и скомпилированный объектный файл, может быть очень сложно решить, является ли ошибка вашей ошибкой или их. Также, глядя на источник в как правильно использовать и расширять библиотеку, которую документация может не охватывать. Не все отправляют MSDN со своей библиотекой. И у великих инженеров-программистов есть неприятная привычка делать с вашим кодом то, о чем вы никогда не мечтали. ;-)


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

подумайте о C, когда это не требуется. В то время компиляторы не обрабатывали спецификацию возвращаемого типа по умолчанию для int. Теперь предположим, что у вас была функция foo (), который вернул указатель на void. Однако, поскольку у вас не было объявления, компилятор подумает, что он должен вернуть целое число. На некоторых Motorola системы, например, integeres и указатели будут быть возвращены в разных регистрах. Теперь компилятор больше не будет использовать правильный регистр и вместо этого вернет указатель на целое число в другом регистре. Момент, когда вы пытаетесь работать с этим указателем-все ад.

объявление функции в заголовок в порядке. Но помните, если вы объявляете и определяете в заголовке, убедитесь, что они встроены. Один из способов достичь этого-поместить определение внутри определения класса. В противном случае добавьте inline ключевое слово. В противном случае вы столкнетесь с нарушением ODR, когда заголовок включен в несколько файлов реализации.


у вас в основном есть 2 представления о классе / функции / что угодно:

объявление, в котором вы объявляете имя, параметры и члены (в случае структуры/класса), и определение, в котором вы определяете, что делают функции.

Среди минусов повторение, еще одно большое преимущество заключается в том, что вы можете объявить функцию как int foo(float f) и оставьте детали в реализации (=определение), поэтому любой, кто хочет использовать вашу функцию foo просто включает в себя файл заголовка и ссылки на вашу библиотеку/objectfile, поэтому пользователи библиотеки, а также компиляторы просто должны заботиться о определенном интерфейсе, который помогает понять интерфейсы и ускоряет время компиляции.


одно преимущество, которое я еще не видел: API

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

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


преимущество

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


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

Если ClassA.h должен ссылаться на элемент данных в ClassB.h, вы можете часто использовать только прямые ссылки в ClassA.h и включить ClassB.h in ClassA.cc а не в классе.h, тем самым сокращая зависимость времени компиляции.

для больших систем это может быть огромная экономия времени на сборку.


  1. разъединение дает чистый, uncluttered взгляд элементов программы.
  2. возможность создания и ссылки на двоичные модули / библиотеки без раскрытия источников.
  3. связать двоичные файлы без перекомпиляции источников.

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


минус

Это приводит к большому количеству повторений. Большая часть сигнатуры функции должна быть помещена в два или более (как отметил Паулиус) мест.