Что определяет порядок инициализации членов класса, если он отличается от порядка в списке конструктора?
Порядок инициализации определяется не списком мем-инициализаторов, а устройством класса: сначала виртуальные базовые классы, затем прямые базовые классы, затем нестатические поля в порядке их объявления. Только после этого выполняется тело конструктора. Поэтому перестановка элементов в списке конструктора не меняет порядок и может привести к использованию ещё не инициализированного поля.
Класс может содержать базовые части и поля, которые зависят друг от друга. C++ задаёт единый детерминированный порядок их создания, чтобы время жизни подобъектов не зависело от субъективного порядка записи конструктора.
Такой подход также позволяет корректно уничтожать объект: деструктор выполняется для самого объекта, затем поля уничтожаются в обратном порядке объявления, а базовые классы — в порядке, обратном их конструированию.
Разработчик может расположить мем-инициализаторы в порядке зависимостей, ожидая, что именно он будет использован. Это ошибочное предположение: компилятор всё равно инициализирует поля согласно их объявлениям в классе.
Если одно поле вычисляется через другое, объявленное позже, чтение второго поля до его инициализации может привести к неопределённому поведению. Обычно компилятор предупреждает о несовпадении порядка, но полагаться только на предупреждения нельзя.
Полный порядок такой:
Значение в списке конструктора определяет способ инициализации конкретного подобъекта, но не его позицию в последовательности. Если поле отсутствует в списке, используется его инициализатор по умолчанию или default member initializer; для встроенных типов это может означать отсутствие инициализации.
Несмотря на порядок в списке, сначала инициализируется first_, затем second_, поэтому second_ получает значение 11. Хорошая практика — записывать мем-инициализаторы в том же порядке, что и объявления полей: это делает зависимости очевидными и устраняет предупреждения компилятора.
Поля уничтожаются в обратном порядке объявления, поэтому поле, объявленное раньше, должно быть готово к уничтожению после полей, объявленных позже. Это особенно важно для ресурсов и объектов-обёрток, где один член зависит от времени жизни другого.
В классе есть конфигурация, которая должна использовать журналирование. Если поле журнала объявлено после конфигурации, но в списке конструктора журнал записан первым, это не исправляет зависимость: конфигурация всё равно создаётся раньше журнала.
Возможны два решения. Можно изменить порядок мем-инициализаторов, но это не повлияет на семантику и лишь создаст ложное ощущение корректности. Можно переставить объявления полей так, чтобы журнал был объявлен раньше конфигурации; это действительно изменит порядок создания и отражает зависимость.
Предпочтительно второе решение. Оно делает порядок инициализации корректным, согласует его с порядком уничтожения и уменьшает риск неопределённого поведения при последующих изменениях класса.
Нет. Он определяет только выражение или конструктор, используемые для инициализации каждого поля. Последовательность всегда задаётся объявлениями базовых классов и полей.
Сначала используется default member initializer, если он задан. Иначе поле инициализируется по правилам default-initialization: для встроенного типа значение может остаться неопределённым, а для объекта класса вызывается подходящий конструктор по умолчанию.
Зависимый ресурс должен уничтожаться раньше ресурса, от которого он зависит. Поскольку поля уничтожаются в обратном порядке объявления, поле-основание зависимости следует объявлять раньше зависимого поля. Иначе при уничтожении зависимый объект может обратиться к уже разрушенному ресурсу.