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