Каковы преимущества и недостатки разделения объявления и определения, как в C ++? - PullRequest
9 голосов
/ 14 марта 2009

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

function someFunc();

function someFunc()
{
  //Implementation.
}

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

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

Ответы [ 11 ]

14 голосов
/ 14 марта 2009

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

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

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

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

11 голосов
/ 14 марта 2009

Это артефакт работы компиляторов 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-, но для этого предназначены защитные элементы.

Преимущества:

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

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

3 голосов
/ 14 марта 2009

Существует два основных преимущества разделения объявления и определения на заголовочные и исходные файлы C ++. Во-первых, вы избегаете проблем с правилом One Definition , когда ваш класс / функции / все остальные * #include d находятся в нескольких местах. Во-вторых, поступая таким образом, вы разделяете интерфейс и реализацию. Пользователям вашего класса или библиотеки нужно только увидеть ваш заголовочный файл, чтобы написать код, который его использует. Вы также можете сделать еще один шаг вперед с помощью Pimpl Idiom и сделать так, чтобы пользовательскому коду не приходилось перекомпилировать каждый раз, когда изменяется реализация библиотеки.

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

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

2 голосов
/ 14 марта 2009

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

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

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

1 голос
/ 16 марта 2009

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

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

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

1 голос
/ 14 марта 2009

У вас в основном есть два взгляда на класс / функцию / что угодно:

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

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

0 голосов
/ 17 марта 2009

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

0 голосов
/ 16 марта 2009
  1. Разделение обеспечивает чистое, беспорядочное представление элементов программы.
  2. Возможность создания и ссылки на двоичные модули / библиотеки без раскрытия источников.
  3. Ссылка на двоичные файлы без перекомпиляции источников.
0 голосов
/ 14 марта 2009

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

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

Для больших систем это может значительно сэкономить время при сборке.

0 голосов
/ 14 марта 2009

Неудобство

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...