Объекты уровня пространств имен в разных единицах компиляции не должны зависеть друг от друга при инициализации, поскольку порядок их инициализации не определен. В противном случае вам обеспечена головная боль при попытках разобраться со сбоями в работе программы после внесения небольших изменений в ваш проект и невозможностью его переноса даже на новую версию того же самого компилятора.
Когда вы определяете два объекта уровня пространства имен в разных единицах компиляции, конструктор какого из объектов будет вызван первым, не определено. Чаще всего (но не всегда) ваш инструментарий будет инициализировать их в том порядке, в котором компонуются скомпилированные объектные файлы, но полагаться на это предположение нельзя, даже если оно и выполняется, — вы же не хотите, чтобы корректность вашей программы зависела от вашего файла проекта или makefilе
(дополнительную информацию о неприятностях, связанных с зависимостью от порядка, можно почерпнуть в рекомендации 59).
Таким образом, в коде инициализации любого объекта уровня пространства имен вы не можете полагаться на то, что уже инициализирован некоторый объект, определенный в другой единице компиляции. Это же касается и динамически инициализируемых переменных примитивных типов (пример такого кода на уровне пространства имен: bool reg_success = LibRegister('mylib');
).
Заметим, что еще до того, как будет вызван конструктор, объект на уровне пространства имен статически инициализируется нулями (в отличие от, скажем, автоматического объекта, который обычно содержит мусор). Парадоксально, но эта инициализация нулями может затруднить обнаружение ошибки, поскольку вместо аварийного завершения программы такой заполненный нулями (но на самом деле неинициализированный) объект создает видимость корректности. Вам кажется, что строка пуста, указатель имеет нулевое значение, целое число равно нулю, — в то время как на самом деле никакой код еще и не пытался инициализировать ваши объекты.
Чтобы решить эту проблему, избегайте переменных уровня пространства имен везде, где только можно; такие переменные — вообще опасная практика (см. рекомендацию 10). Если вам нужна такая переменная, которая может зависеть от другой, подумайте о применении шаблона проектирования Singleton; при аккуратном его использовании можно избежать неявных зависимостей, обеспечивая инициализацию объекта при его первом использовании. Singleton остается глобальной переменной в шкуре овцы (еще раз см. рекомендацию 10), и не работает при взаимных или циклических зависимостях (и здесь инициализация нулями также способствует общей неразберихе).
22. Минимизируйте зависимости определений и избегайте циклических зависимостей
Избегайте излишних зависимостей. Не включайте при помощи директивы #include
определения там, где достаточно предварительного объявления.
Избегайте взаимозависимостей. Циклические зависимости возникают, когда два модуля непосредственно или опосредованно зависят друг от друга. Модуль представляет собой обособленную единицу; взаимозависимые модули не являются полностью отдельными модулями, будучи по сути частями одного большего модуля. Таким образом, циклические зависимости являются противниками модульности и представляют угрозу большим проектам. Избегайте их.
Предпочитайте использовать предварительные объявления везде, где не требуется полное определение типа. Полное определение класса С требуется в двух основных случаях.
•
•
Не будем рассматривать в этой книге тривиальные случаи циклических зависимостей, которые приводят к ошибкам компиляции — думаем, вы успешно справитесь с ними, воспользовавшись многими хорошими советами, имеющимися в литературе и рекомендации 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 приводят к такому виду зависимости, которая допустима только в исключительно стабильных иерархиях. В противном случае вам лучше изменить