Стоит ли заранее объявлять библиотечные классы?

Я только начал изучать Qt, используя их учебник. В настоящее время я работаю над уроком 7, где мы создали новый класс LCDRange. Реализация LCDRange (файл .cpp) использует класс Qt QSlider, поэтому в файле .cpp

#include <QSlider>

но в заголовке есть предварительное объявление:

class QSlider;

Согласно Qt,

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

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

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


person Skilldrick    schedule 26.04.2009    source источник


Ответы (8)


Абсолютно. Модель сборки C/C++ - это... гм... анахронизм (если не сказать больше). Для крупных проектов это становится серьезной PITA.

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

Отказ от включаемых ссылок — это одна из причин, по которой вы должны использовать предварительные объявления.

// a.h
#include "b.h"
struct A { B * a;  }

// b.h
#include "a.h"  // circlular include reference 
struct B { A * a;  }

// Solution: break circular reference by forward delcaration of B or A

Сокращение времени перестроения. Представьте себе следующий код.

// foo.h
#include <qslider>
class Foo
{
   QSlider * someSlider;
}

теперь каждый файл .cpp, который прямо или косвенно извлекает Foo.h, также извлекает QSlider.h и все его зависимости. Это могут быть сотни файлов .cpp! (Предварительно скомпилированные заголовки немного помогают, а иногда и сильно, но они превращают нагрузку на диск/ЦП в нагрузку на память/диск и, таким образом, вскоре достигают «следующего» предела)

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

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

// bar.h
#include "foo.h"
class Bar 
{
   Foo * kungFoo;
   // ...
}

Теперь, если большинству ваших файлов .cpp требуется извлекать bar.h, они также косвенно извлекают foo.h. Таким образом, каждое изменение foo.h запускает сборку всех этих файлов .cpp (которые могут даже не знать Foo!). Если вместо этого bar.h использует предварительное объявление для Foo, зависимость от foo.h ограничивается bar.cpp:

// bar.h
class Foo;
class Bar 
{
   Foo * kungFoo;
   // ...
}

// bar.cpp
#include "bar.h"
#include "foo.h"
// ...

Это настолько распространено, что это шаблон — шаблон PIMPL. . Его использование двоякое: во-первых, он обеспечивает настоящую изоляцию интерфейса/реализации, а во-вторых, уменьшает зависимости сборки. На практике я бы оценил их полезность 50:50.

Вам нужна ссылка в заголовке, у вас не может быть прямого экземпляра зависимого типа. Это ограничивает случаи, когда могут применяться предварительные объявления. Если вы делаете это явно, обычно используется служебный класс (например, boost::scoped_ptr).

Стоит ли время сборки? Определенно, я бы сказал. В худшем случае время сборки полиномиально пропорционально количеству файлов в проекте. другие методы, такие как более быстрые машины и параллельные сборки, могут обеспечить только процентный выигрыш.

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

На практике управление временем сборки, хотя и необходимо для большого проекта (скажем, сотни исходных файлов), все же дает «удобство» для небольших проектов. Кроме того, добавление улучшений постфактум часто требует терпения, поскольку одно исправление может сократить 40-минутную сборку всего на несколько секунд (или меньше).

person peterchen    schedule 26.04.2009
comment
Если то, что вы сказали об экспоненциальном увеличении времени сборки, было правдой, проект, над которым я работаю (который содержит около 100 исходных файлов), должен компилироваться с нуля примерно за 2 ^ 100 секунд, а вместо этого это занимает около минуты. - person ; 26.04.2009
comment
@Neil: для каждого добавляемого файла время сборки увеличивается пропорционально размеру проекта (поскольку для компиляции нового файла необходимо обработать некоторое количество заголовков, пропорциональное размеру проекта). Это звучит экспоненциально для меня. - person Jay Conrod; 26.04.2009
comment
уточнено: это, конечно, сценарий наихудшего случая (кодоемкие, сильно взаимозависимые заголовки). По моему опыту, скорость сборки падает по спирали, как только вы начинаете достигать пределов производительности машины, и все, что вы можете сделать, это найти компромисс между узкими местами. --- Я ожидаю, что пассивное пренебрежение, как правило, приведет к (только) полиномиальному росту, но неправильные ожидания или неподходящие стандарты кодирования могут привести вас к экспоненциальной сливной яме. - person peterchen; 26.04.2009
comment
@jay - для меня это звучит линейно - person ; 26.04.2009
comment
Время сборки на единицу перевода становится O(N), поэтому время сборки всей программы становится O(N*N) — полиномиальное, а не экспоненциальное. Пассивное пренебрежение по сравнению с наихудшим случаем, вероятно, является константой 0‹f‹1, поэтому нет большой разницы - person MSalters; 27.04.2009

Я пользуюсь этим все время. Мое правило заключается в том, что если заголовок не нужен, я помещаю предварительную декларацию ("используйте заголовки, если необходимо, используйте предварительные декларации, если можете"). Единственное, что отстой, это то, что мне нужно знать, как был объявлен класс (структура/класс, может быть, если это шаблон, мне нужны его параметры,...). Но в подавляющем большинстве случаев все сводится к "class Slider;" или чему-то в этом роде. Если что-то требует дополнительных хлопот, чтобы просто объявить, всегда можно объявить специальный заголовок прямого объявления, как это делает Стандарт с iosfwd.

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

Это примерный план:

/* --- --- --- Y.hpp */
class X;
class Y {
    X *x;
};

/* --- --- --- Y.cpp */
#include <x.hpp>
#include <y.hpp>

...

Существуют интеллектуальные указатели, специально предназначенные для работы с указателями на неполные типы. Одним из очень известных является boost::shared_ptr.

person Johannes Schaub - litb    schedule 26.04.2009
comment
Я всегда следовал этой стратегии, и она помогает сократить время компиляции. - person Naveen; 26.04.2009

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

Посмотрите FAQ 39.12 и 39.13

person dirkgently    schedule 26.04.2009
comment
Спасибо! Единственное, что я знал до сих пор о предварительно скомпилированных заголовках, это то, что руководство по SDL, которому я следовал, началось с того, что мне сказали отключить их в Visual Studio... - person Skilldrick; 26.04.2009
comment
Предварительно скомпилированные заголовки очень полезны для заголовков внешних библиотек (т. е. стабильных заголовков). Я бы не стал отключать их вслепую. Тем не менее, они могут лишь частично решить проблемы плохой иерархии включения. - person peterchen; 26.04.2009

Стандартная библиотека делает это для некоторых классов iostream в стандартном заголовке <iosfwd>. Однако это не общеприменимый метод - обратите внимание, что для других стандартных типов библиотек нет таких заголовков, и он не должен (ИМХО) быть вашим подходом по умолчанию к разработке иерархий классов.

Хотя это кажется любимой «оптимизацией» для программистов, я подозреваю, что, как и в случае с большинством оптимизаций, немногие из них на самом деле рассчитали время сборки своих проектов как с такими объявлениями, так и без них. Мои ограниченные эксперименты в этой области показывают, что использование предварительно скомпилированных заголовков в современных компиляторах делает их ненужными.

person Community    schedule 26.04.2009
comment
+1: для этого надо не по умолчанию подходить, а мерить. - person peterchen; 26.04.2009
comment
Согласен с замечанием по оптимизации. Я обнаружил, что при распределенных сборках фаза (параллельной) компиляции имеет тенденцию быть короче, чем фаза (однопоточной) компоновки, особенно при использовании оптимизации всей программы. Упреждающие объявления могут сократить время добавочной сборки, но это сложнее измерить, и это не влияет на ночную сборку моей команды. - person bk1e; 26.04.2009

Существует ОГРОМНАЯ разница во времени компиляции для больших проектов, даже с тщательно управляемыми зависимостями. Вам лучше привыкнуть к предварительному объявлению и максимально избегать заголовочных файлов, потому что во многих магазинах программного обеспечения, использующих C ++, это требуется. Причина, по которой вы не видите всего этого в стандартных файлах заголовков, заключается в том, что они интенсивно используют шаблоны, и в этот момент упреждающее объявление становится затруднительным. Для MSVC вы можете использовать /P, чтобы посмотреть, как выглядит предварительно обработанный файл перед фактической компиляцией. Если вы не сделали никакого предварительного объявления в своем проекте, вероятно, было бы интересно увидеть, сколько дополнительной обработки необходимо выполнить.

person Community    schedule 26.04.2009

В общем, нет.

Раньше я пересылал декларации столько, сколько мог, но не больше.

Что касается Qt, вы можете заметить, что существует <QtGui> включаемый файл, который будет извлекать все виджеты GUI. Кроме того, есть <QtCore>, <QtWebKit>, <QtNetwork> и т. д. Для каждого модуля есть заголовочный файл. Кажется, команда Qt также считает этот метод предпочтительным. Они так говорят в своей документации модуля.

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

person Mark Beckwith    schedule 26.04.2009
comment
я думаю это другое дело. Часто имеет смысл сгруппировать заголовки вместе для удобства. Как и boost (несколько больших заголовков включают в себя много маленьких заголовков), и Qt, как вы показываете, тоже. Но не имеет особого смысла включать тысячи строк, когда все, что вам нужно, — это одна единственная строка кода, которая делает тип известным как тип класса. - person Johannes Schaub - litb; 26.04.2009
comment
например, если вы заглянете в QtGui/qregion.h, вы увидите, что он не включает весь qvector.hpp, потому что ему нужен QVector в возвращаемом типе одной функции-члена. Но он использует шаблон предварительного объявления ‹class T› class QVector; вместо. - person Johannes Schaub - litb; 26.04.2009
comment
Фу. Я знал, что меня за это проголосуют. Кажется, ТАК - это получение самого популярного ответа, а не самого правильного. - person Mark Beckwith; 26.04.2009
comment
@mark - Странные понятия, к которым люди эмоционально привязаны, не так ли? Материнство и яблочный пирог я вроде как понимаю, но вперед заявления???? - person ; 26.04.2009

Когда ты пишешь...

включить foo.h

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

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

Правила C++ позволяют повторно открывать пространство имен в любой точке единицы компиляции (в отличие от структуры или класса) для поддержки переадресации. декларация.

person Thomas L Holaday    schedule 26.04.2009

Прямые объявления очень полезны для разрыва циклических зависимостей, и иногда их можно использовать с вашим собственным кодом, но их использование с библиотечным кодом может нарушить работу программы на другой платформе или с другими версиями библиотеки (это произойдет даже с вашим код, если вы недостаточно осторожны). ИМХО не стоит.

person cube    schedule 26.04.2009