Стоит ли заранее объявлять библиотечные классы? - PullRequest
8 голосов
/ 26 апреля 2009

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

#include <QSlider>

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

class QSlider;

Согласно Qt,

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

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

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

Ответы [ 8 ]

17 голосов
/ 26 апреля 2009

Абсолютно. Модель сборки 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-минутной сборки.

8 голосов
/ 26 апреля 2009

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

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

Это приблизительный план:

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

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

...

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

3 голосов
/ 26 апреля 2009

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

Поиск FAQ 39.12 и 39.13

2 голосов
/ 26 апреля 2009

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

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

1 голос
/ 26 апреля 2009

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

0 голосов
/ 26 апреля 2009

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

включает "foo.h"

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

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

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

0 голосов
/ 26 апреля 2009

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

0 голосов
/ 26 апреля 2009

В общем, нет.

Раньше я объявлял столько, сколько мог, но больше нет.

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

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

...