Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ - Скотт Майерс
Шрифт:
Интервал:
Закладка:
Да никак! Еще раз повторю: относительный порядок инициализации нестатических локальных объектов, определенных в разных единицах трансляции, не определен. На то есть своя причина. Определить «правильный» порядок инициализации нелокальных статических объектов трудно. Очень трудно. Неразрешимо трудно. В наиболее общем случае – при наличии многих единиц трансляции и нелокальных статических объектов, сгенерированных путем неявной конкретизации шаблонов (которые и сами могут быть результатом неявной конкретизации других шаблонов) – не только невозможно определить правильный порядок инициализации, но обычно даже не стоит искать частные случаи, когда этот порядок в принципе определить можно.
К счастью, небольшое изменение в проекте программы позволяет полностью устранить эту проблему. Нужно лишь переместить каждый нелокальный статический объект в отдельную функцию, в которой он будет объявлен статическим. Эти функции возвращают ссылки на объекты, которые в них содержатся. Клиенты затем вызывают функции вместо непосредственного обращения к объектам. Другими словами, нелокальные статические объекты заменяются локальными статическими объектами (знакомые с паттернами проектирования легко узнают в этом описании типичную реализацию паттерна Singleton).
Этот подход основан на том, что C++ гарантирует: локальные статические объекты инициализируются в первый раз, когда определение объекта встречается при вызове этой функции. Поэтому если вы замените прямой доступ к нелокальным статическим объектам вызовом функций, возвращающих ссылки на расположенные внутри них локальные статические объекты, то можете быть уверены, что ссылки, возвращаемые из функций, будут ссылаться на инициализированные объекты. Дополнительное преимущество заключается в том, что если вы никогда не вызываете функцию, эмулирующую нелокальный статический объект, то и не придется платить за создание и уничтожение объекта, чего не скажешь о реальных нелокальных статических объектах.
Вот как этот прием применяется к объектам tfs и tempDir:
class FileSystem {...}; // как раньше
FileSystem& tfs() // эта функция заменяет объект tfs, она может
{ // быть статической в классе FileSystem
static FileSystem fs; // определение и инициализация локального
// статического объекта
return fs; // возврат ссылки на него
}
class Directory {...}; // как раньше
Directory::Directory( params ) // как раньше, но вместо ссылки на tfs
{ // вызов tfs()
...
std::size_t disks = tfs().numDisks();
...
}
Directory& tempDir() // эта функция заменяет объект tempDir,
{ // может быть статической в классе Directory
static Directory td; // определение/инициализация локального
// статического объекта
return td; // возврат ссылки на него
}
Клиенты работают с этой модифицированной программой так же, как раньше, за исключением того, что вместо tfs и tempDir они теперь обращаются к tfs() и tempDir(). Иными словами, используют ссылки на объекты, возвращенные функциями, вместо использования самих объектов.
Функции, которые в соответствии с данной схемой возвращают ссылки, всегда просты: определить и инициализировать локальный статический объект в строке 1 и вернуть его в строке 2. В связи с этим у вас может возникнуть искушение объявить их встроенными, особенно, если они часто вызываются (см. правило 30). С другой стороны, тот факт, что эти функции содержат в себе статические объекты, усложняет их применение в многопоточных системах. Но тут никуда не деться: неконстантные статические объекты любого рода – локальные или нелокальные – представляют проблему в случае наличия в программе нескольких потоков. Решить ее можно, например, вызвав самостоятельно все функции, возвращающие ссылки, на этапе запуска программы, когда еще работает только один поток. Это исключит неопределенность в ходе инициализации.
Конечно, применимость идеи функций, возвращающих ссылки, для предотвращения проблем, связанных с порядком инициализации, зависит от того, существует ли в принципе разумный порядок инициализации ваших объектов. Если вы напишете код, в котором объект A должен быть инициализирован прежде, чем объект B, и одновременно сделаете инициализацию A зависимой от инициализации B, то вас ждут проблемы – и поделом! Если, однако, вы будете избегать таких патологических ситуаций, то описанная схема сослужит вам добрую службу, по крайней мере, в однопоточных приложениях.
Таким образом, чтобы избежать использования объектов до их инициализации, вам следует сделать три вещи. Первое: вручную инициализировать не являющиеся членами объекты встроенных типов. Второе: использовать списки инициализации членов для всех частей объекта. И наконец, третье: обойти за счет правильного проектирования проблему негарантированного порядка инициализации нелокальных статических объектов, определенных в разных единицах трансляции.
Что следует помнить• Всегда вручную инициализировать объекты встроенных типов, поскольку C++ делает это, только не всегда.
• В конструкторе отдавать предпочтение применению списков инициализации членов перед прямым присваиванием значений в теле конструктора. Перечисляйте данные-члены в списке инициализации в том же порядке, в каком они объявлены в классе.
• Избегайте проблем с порядком инициализации в разных единицах трансляции, заменяя нелокальные статические объекты локальными статическими объектами.
Глава 2
Конструкторы, деструкторы и операторы присваивания
Почти во всех ваших классах будут определены один или несколько конструкторов, деструктор и оператор присваивания. Это функции, которые отвечают за операции создания и инициализации объекта, его уничтожения, а также присваивания ему нового значения. Ошибки в этих функциях приводят к далеко идущим и неприятным последствиям и отражаются на всех ваших классах, поэтому они должны быть написаны правильно. В настоящей главе изложены правила по программированию функций, составляющих основу классов.
Правило 5: Какие функции C++ создает и вызывает молча
Когда пустой класс перестает быть пустым? Когда за него берется C++. Если вы не объявите конструктор копирования, оператор присваивания или деструктор самостоятельно, то компилятор сделает это за вас. Более того, если вы не объявите вообще никакого конструктора, то компилятор автоматически создаст конструктор по умолчанию. Все эти функции будут открытыми и встроенными (см. правило 30). Например, такое объявление:
class Empty {};
эквиваленто следующему:
class Empty {
public:
Empty() {...} // конструктор по умолчанию
Empty(const Empty& rhs) {...} // конструктор копирования
~Empty() {...} // деструктор – см. ниже
// о виртуальных деструкторах
Empty& operator=(const Empty& rhs) {...} // оператор присваивания
};
Эти функции генерируются, только если они нужны, но мало найдется случаев, когда без них можно обойтись. Так, следующий код приведет к их автоматической генерации компилятором:
Empty e1; // конструктор по умолчанию;
// деструктор
Empty e2(e1); // конструктор копирования
e2 = e1; // оператор присваивания
Итак, компилятор пишет эти функции для вас, но что они делают? Конструктор по умолчанию и деструктор – это места, в которые компилятор помещает служебный код, например вызов конструкторов и деструкторов базовых классов и нестатических данных-членов. Отметим, что сгенерированный деструктор не является виртуальным (см. правило 7), если только речь не идет о классе, наследующем классу, у которого есть виртуальный деструктор (в этом случае виртуальность наследуется от базового класса).
Что касается конструктора копирования и оператора присваивания, то сгенерированные компилятором версии просто копируют каждый нестатический член данных исходного объекта в целевой. Например, рассмотрим шаблон NamedObject, который позволяет ассоциировать имена с объектами типа T:
Template<typename T>
class NamedObject {
public:
NamedObject(const char *name, const T& value);
NamedObject(const std::string& name, const T& value);
...
private:
std:string nameValue;
T objectValue;
};
Поскольку в классе NamedObject объявлен конструктор, компилятор не станет генерировать конструктор по умолчанию. Это важно. Значит, если вы спроектировали класс так, что его конструктору обязательно должны быть переданы какие-то аргументы, то вам не нужно беспокоиться, что компилятор проигнорирует ваше решение и по собственной инициативе добавит еще и конструктор без аргументов.
В классе NamedObject нет ни конструктора копирования, ни оператора присваивания, поэтому компилятор сгенерирует их (при необходимости). Посмотрите на следующее употребление конструктора копирования: