Герб Саттер - Стандарты программирования на С++. 101 правило и рекомендация
Предпочитайте использовать предварительные объявления везде, где не требуется полное определение типа. Полное определение класса С требуется в двух основных случаях.
• Когда вам необходимо знать размер объекта С. Например, при создании объекта С в стеке или при использовании его в качестве непосредственно хранимого (не через указатель) члена другого класса.
• Когда вам требуется имя или вызов члена С. Например, когда вы вызываете функцию-член этого класса.
Не будем рассматривать в этой книге тривиальные случаи циклических зависимостей, которые приводят к ошибкам компиляции — думаем, вы успешно справитесь с ними, воспользовавшись многими хорошими советами, имеющимися в литературе и рекомендации 1. Мы же обратимся к циклическим зависимостям, которые остаются в компилируемом коде, и посмотрим, как они влияют на его качество и какие действия следует предпринять, чтобы избежать их.
В общем случае зависимости и их циклы следует рассматривать на уровне модулей. Модуль представляет собой образующий единое целое набор совместно опубликованных классов и функций (см. рекомендацию 5). Циклическая зависимость в простейшем виде представляет собой два класса, непосредственно зависящих друг от друга
class Child; // Устранение циклической зависимости
class Parent { // ...
Child* myChild_;
};
// возможно, в другом заголовочном файле
class Child { // ...
Parent* myParent_;
};
Классы Parent и Child зависят друг от друга. Приведенный код компилируется, но мы сталкиваемся с фундаментальной проблемой: эти два класса больше не являются независимыми, и более того, становятся взаимозависимы друг от друга. Это не всегда плохо, но должно иметь место лишь в том случае, когда оба класса являются частями одного и того же модуля (разработанного одним и тем же человеком или командой, оттестированного и выпущенного как единое целое).
В противоположность описанной ситуации, рассмотрим, что будет, если класс Child не должен будет хранить обратную связь с объектом Parent? Тогда Child может быть выпущен в собственном отдельном небольшом модуле (и, возможно, под другим именем), полностью независимо от Parent — что, конечно, существенно повышает гибкость проектирования.
Все становится еще хуже, когда зависимости охватывают несколько модулей. Такой мощный "клей", как зависимости, объединяют эти модуля в единую монолитную публикуемую единицу. Вот почему циклы являются самыми страшными врагами модульности.
Чтобы разорвать циклы, примените принцип инверсии зависимостей (Dependency Inversion Principle), описанный в [Martin96a] и [Martin00] (см. также рекомендацию 36): не делайте модули высокого уровня зависящими от модулей низкого уровня; вместо этого делайте их зависимыми от абстракций. Если вы можете определить независимые абстрактные классы для Parent или Child, вы разорвете цикл. В противном случае вы должны сделать их частями одного и того же модуля.
Частный вид зависимости, от которой страдают некоторые проекты, — это транзитивная зависимость от производных классов, которая осуществляется, когда базовый класс зависит от всех своих наследников, прямых и непрямых. Ряд реализаций шаблона проектирования Visitor приводят к такому виду зависимости, которая допустима только в исключительно стабильных иерархиях. В противном случае вам лучше изменить свой проект, например, воспользовавшись шаблоном проектирования Acyclic Visitor [Martin98].
Одним из симптомов чрезмерных зависимостей является перекомпиляция больших частей проекта при внесении небольших локальных изменений (см. рекомендацию 2).
ИсключенияЦиклические зависимости между классами — не всегда плохо, пока классы рассматриваются как часть одного модуля и совместно тестируются и выпускаются. Простая непосредственная реализация таких шаблонов проектирования, как Command и Visitor приводят к интерфейсам, которые естественным образом оказываются взаимозависимыми. Такие зависимости можно разрушить, но это требует более четкого проектирования.
Ссылки[Alexandrescu01] §3 • [Boost] • [Gamma95] • [Lakos96] §0.2.1, §4.6-14, §5 • [Martin96a] • [Martin96b] • [Martin98] §7 • [Martin00] • [McConnell93] §5 • [Meyers97] §46 • [Stroustrup00] §24.3.5 • [Sutter00] §26 • [Sutter02] §37 • [Sutter03]
23. Делайте заголовочные файлы самодостаточными
Убедитесь, что каждый написанный вами заголовочный файл компилируем самостоятельно, т.е. что он включает все заголовочные файлы, от которых зависит его содержимое.
ОбсуждениеЕсли один заголовочный файл не работает, пока не включен другой заголовочный файл, проект получается очень неуклюжим, а на пользователя возлагается дополнительная задача следить за тем, какие заголовочные файлы надо включить в исходный текст.
Раньше некоторые эксперты советовали, чтобы заголовочные файлы не включали другие заголовочные файлы из-за накладных расходов на многократное открытие и анализ заголовочных файлов, защищенных директивами препроцессоров от повторной обработки. К счастью, сейчас этот совет устарел. Многие современные компиляторы С++ распознают соответствующую защиту заголовочных файлов автоматически (см. рекомендацию 24) и просто не открывают один и тот же заголовочный файл дважды. Некоторые компиляторы используют предкомпиляцию заголовочных файлов, которая позволяет избежать анализа часто используемых заголовочных файлов.
Однако не включайте заголовочные файлы, в которых вы не нуждаетесь, так как это напрасно создает паразитные зависимости.
Для гарантии самодостаточности заголовочных файлов скомпилируйте каждый из них отдельно от других и убедитесь, что это не приводит к ошибкам или предупреждениям.
ПримерыРяд тонких моментов возникает в связи с использованием шаблонов.
Пример 1. Зависимые имена. Шаблоны компилируются в точке, где они определены, с тем исключением, что все зависимые имена или типы не компилируются до точки инстанцирования. Это означает, что template<class T> class Widget с членом std::deque<T> не приведет к ошибке времени компиляции, даже если заголовочный файл <deque> не включен — если при этом не происходит инстанцирование Widget. Поскольку очевидно, что шаблон Widget существует для того, чтобы быть инстанцированным, его заголовочный файл должен содержать строку #include <deque>.
Пример 2. Шаблоны функций-членов и функции-члены шаблонов инстанцируются только при использовании. Предположим, что Widget не имеет члена типа std::deque<T>, но функция-член Widget с именем Transmogrify использует deque. Тогда вызывающая Widget функция может инстанцировать и использовать Widget, даже если заголовочный файл <deque> не включен — до тех пор, пока не используется функция-член Transmogrify. По умолчанию заголовочный файл Widget все же должен включать <deque>, поскольку это необходимо, по крайней мере, для некоторых пользователей Widget. В редких случаях, когда большой заголовочный файл включается только для обеспечения работоспособности нескольких редко используемых функций шаблона, следует подумать о том, чтобы сделать эти функции не членами и вынести их в отдельный заголовочный файл, включающий упомянутый большой файл (см. рекомендацию 44).
Ссылки[Lakos96] §3.2 • [Stroustrup00] §9.2.3 • [Sutter00] §26-30 • [Vandevoorde03] §9-10
24. Используйте только внутреннюю, но не внешнюю защиту директивы #include
Предотвращайте непреднамеренное множественное включение ваших заголовочных файлов директивой #include, используя в них защиту с уникальными именами.
ОбсуждениеКаждый заголовочный файл должен использовать внутреннюю защиту директивы #include, чтобы предотвратить переопределения в случае множественного включения данного файла. Например, заголовочный файл fоо.h должен иметь такой общий вид:
#ifndef FOO_H_INCLUDED_
#define FOO_H_INCLUDED_
// ... Содержимое файла …
#endif
Обратите внимание на следующие правила при определении защиты включения.
• Используйте для защиты уникальные имена. Убедитесь, что вы используете имена, уникальные, по крайней мере, в пределах вашего приложения. Выше мы использовали одно распространенное соглашение для используемых в защите имен; имена для защиты могут включать имя приложения, а некоторые инструменты генерируют имена для защиты, содержащие случайные числа.
• Не пытайтесь хитрить. Не размещайте никакого кода или комментариев до и после защищенной части, и следуйте показанной выше стандартной форме защиты. Современные препроцессоры могут обнаружить защиту, но могут оказаться малоинтеллектуальными и ожидать кода защиты строго в начале и в конце заголовочных файлов.