В классе один RAII-ресурс зависит от другого. Как порядок объявления полей определяет корректность их уничтожения?
Поля объекта уничтожаются в порядке, обратном порядку их объявления в классе. Поэтому зависимый ресурс нужно объявлять позже ресурса, от которого он зависит: тогда при уничтожении он будет освобождён первым.
Порядок перечисления полей в списке инициализации конструктора этого не меняет. Нарушение ожидаемого порядка может привести к обращению к уже освобождённому ресурсу или к некорректному завершению операции освобождения.
RAII использует детерминированное завершение времени жизни объектов для автоматического освобождения ресурсов. Такой подход решает проблему ручного освобождения памяти, файлов, блокировок и других ресурсов на всех путях выхода из функции, включая исключения.
Для составных объектов этот принцип требует согласованного порядка: ресурс, используемый другим ресурсом, должен жить дольше зависимого объекта. Правило обратного порядка уничтожения делает такую зависимость выразимой прямо через порядок объявления полей.
Предположим, объект-обёртка выполняет операцию над соединением с базой данных в своём деструкторе. Если соединение будет уничтожено раньше обёртки, деструктор обёртки использует недействительный ресурс.
Такая ошибка может быть неочевидной: список инициализации визуально задаёт один порядок, но стандарт C++ игнорирует его при выборе порядка инициализации полей. В результате программа может работать случайно, пока изменение объявления полей или компилятора не проявит ошибку.
Поля инициализируются в порядке их объявления, затем выполняется тело конструктора. При разрушении сначала выполняется тело деструктора, после чего поля уничтожаются в обратном порядке объявления.
Например:
Этот пример некорректен: connection объявлен раньше transaction, поэтому он инициализируется раньше, но список инициализации всё равно требует учитывать фактический порядок. Более того, в приведённой записи transaction(*connection) вычисляется до инициализации connection только потому, что фактический порядок определяется объявлениями, а connection уже должен быть инициализирован первым; однако передача ссылки на объект, управляемый ещё не инициализированным владельцем, делает конструкцию хрупкой и плохо читаемой.
Безопаснее явно выразить зависимость так:
Здесь connection создаётся первым, а при разрушении transaction уничтожается первым. Затем уничтожается connection, поэтому деструктор Transaction может безопасно использовать соединение. На практике предпочтительнее хранить зависимость в типах и интерфейсах так, чтобы деструктор не требовал ручного соблюдения скрытого контракта.
Порядок базовых классов также определяется объявлением: базовые классы инициализируются до полей, а уничтожаются после них, в обратном порядке. Тело деструктора выполняется до автоматического уничтожения полей, поэтому доступ к полям из тела деструктора ещё возможен, но после выхода из него поля уже начинают разрушаться.
В серверном компоненте есть журнал транзакции и файловый дескриптор. Журнал при уничтожении дописывает финальную запись в файл, поэтому дескриптор должен оставаться действительным дольше журнала.
Вариант с ручным вызовом close в деструкторе компонента плох: он дублирует ответственность RAII, усложняет обработку исключений и может нарушить порядок освобождения. Вариант с полями, объявленными в произвольном порядке, компилируется, но создаёт скрытую зависимость от структуры класса.
Выбранное решение — объявить сначала владеющий дескриптор, затем журнал, а освобождение каждого ресурса поручить его RAII-обёртке. В результате журнал уничтожается первым, записывает финальное состояние, после чего дескриптор закрывается; изменение тела конструктора не может случайно поменять этот порядок.
Меняет ли порядок полей в списке инициализации порядок их создания?
Нет. Порядок определяется объявлениями полей в классе, а не списком инициализации. Компилятор обычно предупреждает, если эти порядки различаются, потому что такая запись вводит в заблуждение и может скрывать ошибку.
Когда уничтожаются поля относительно тела деструктора?
Сначала выполняется тело деструктора самого объекта. Только после его завершения автоматически уничтожаются поля, причём в обратном порядке объявления. Поэтому обращаться к полю из тела деструктора можно, но нельзя считать, что другие поля будут жить после завершения этого тела.
Достаточно ли правильного порядка объявления, если зависимость выражена ссылкой или сырым указателем?
Нет. Порядок объявления гарантирует только порядок времени жизни самих полей. Он не делает сырые указатели владеющими и не проверяет, что указанный объект действительно существует. Для надёжности зависимость следует представлять через подходящий тип владения, ссылку с явно ограниченным временем жизни или отдельную абстракцию, которая не позволяет использовать ресурс после его освобождения.