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

StackOverflow https://stackoverflow.com/questions/645778

Вопрос

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

function someFunc();

function someFunc()
{
  //Implementation.
}

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

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

Это было полезно?

Решение

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

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

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

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

Другие советы

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

<Ол>
  • Разделение обеспечивает чистое, беспорядочное представление элементов программы.
  • Возможность создавать и связывать двоичные модули / библиотеки без раскрытия источников.
  • Ссылка на двоичные файлы без перекомпиляции источников.
  • Если все сделано правильно, это разделение сокращает время компиляции, когда изменилась только реализация.

    Недостаток

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

    Лицензировано под: CC-BY-SA с атрибуция
    Не связан с StackOverflow
    scroll top