Эта особенность языка обусловлена необходимостью гарантировать единый порядок уничтожения членов; в противном случае деструктор был бы должен уничтожать объекты в разном порядке, в зависимости от того, в каком именно порядке конструктор создавал их. Накладные расходы, необходимые для решения этой проблемы, признаны неприемлемыми.
Решение заключается в том, чтобы всегда писать инициализаторы членов в том же порядке, в котором эти члены объявлены в классе. В этом случае сразу становятся очевидными все некорректные зависимости между членами. Еще лучше полностью избегать зависимости инициализации одного члена от других.
Многие компиляторы (но не все) выдают предупреждение при нарушении этого правила.
Ссылки
[Cline99] §22.03-1] • [Dewhurst03] §52-53 • [Koenig97] §4 • [Lakos96] §10.3.5 • [Meyers97] §13 • [Murray93] §2.1.3 • [Sutter00] §47
48. В конструкторах предпочитайте инициализацию присваиванию
Резюме
В конструкторах использование инициализации вместо присваивания для установки значений переменных-членов предохраняет от ненужной работы времени выполнения при том же объеме вводимого исходного текста.
Обсуждение
Конструкторы генерируют скрытый код инициализации. Рассмотрим следующий код:
class A {
string s1_, s2_;
public:
A() { s1_ = "Hello, "; s2_ = "world"; }
};
В действительности сгенерированный код конструктора выглядит так, как если бы вы написали:
А() : s1_(), s2_() { s1_ = "Hello, "; s2_ = "world"; }
To есть объекты, не инициализированные вами явно, автоматически инициализируются с использованием их конструкторов по умолчанию, после чего выполняется присваивание значений с использованием их операторов присваивания. Чаще всего операторы присваивания нетривиальных объектов выполняют немного больше работы, чем конструкторы, поскольку работают с уже созданными объектами.
Таким образом, инициализация переменных-членов в списке инициализации дает код, лучше выражающий ваши намерения и обычно более быстрый и меньшего размера:
А() : s1_("Hello, "), s2_("world ") { }
Эта методика не является преждевременной оптимизацией; это — избежание преждевременной пессимизации (см. рекомендацию 9).
Исключения
Всегда выполняйте захват неуправляемого ресурса (например, выделение памяти оператором new
, результат которого не передается немедленно конструктору интеллектуального указателя) в теле конструктора, а не в списке инициализации (см. [Sutter02]). Конечно, лучше всего вообще не использовать таких небезопасных и не имеющих владельца ресурсов (см. рекомендацию 13).
Ссылки
[Dewhurst03] §51, §59 • [Keffer95] pp.13-14 • [Meyers97] §12 • [Murray93] §2.1.31 • [Sutter00] §8, §47 • [Sutter02] §18
49. Избегайте вызовов виртуальных функций в конструкторах и деструкторах
Резюме
Внутри конструкторов и деструкторов виртуальные функции теряют виртуальность. Хуже того — все прямые или косвенные вызовы нереализованных чисто виртуальных функций из конструктора или деструктора приводят к неопределенному поведению. Если ваш дизайн требует виртуальной передачи в производный класс из конструктора или деструктора базового класса, следует воспользоваться иной методикой, например, постконструкторами.
Обсуждение
В C++ полный объект конструируется по одному базовому классу за раз.
Пусть у нас есть базовый класс В
и класс D
, производный от B
. При создании объекта D
, когда выполняется конструктор В
, динамическим типом создаваемого объекта является B
. В частности, вызов виртуальной функции B::Fun
приведет к выполнению функции Fun
, определенной в классе В
, независимо от того, перекрывает ее класс D
или нет. И это хорошо, поскольку вызов функции-члена D
в тот момент, когда члены объекта D
еще не инициализированы, может привести к хаосу. Только после завершения выполнения конструктора В
выполняется тело конструктора D
и объект приобретает тип D
. В качестве эмпирического правила следует помнить, что в процессе конструирования В
нет никакого способа определить, является ли В
отдельным объектом или базовой частью некоторого иного производного объекта.
Кроме того, следует помнить, что вызов из конструктора чисто виртуальной функции, не имеющей определения, приводит к неопределенному поведению.
С другой стороны, в некоторых случаях дизайн требует использования "постконструктора", т.е. виртуальной функции, которая должна быть вызвана после того, как полный объект оказывается сконструирован. Некоторые методики, применяемые для решения этой задачи, описаны в приводимых ниже ссылках. Вот (далеко не исчерпывающий) список возможных решений.
Читать дальше