Программирование C++Управление памятьюРазработчик системного программного обеспечения на C++

В конструкторе RAII класса второе поле выбрасывает исключение: какие подобъекты уничтожаются и почему?

В конструкторе RAII-класса второе поле выбрасывает исключение: какие подобъекты уничтожаются и почему?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

При исключении из конструктора уничтожаются все базовые классы и поля, которые успели полностью сконструироваться до точки сбоя, в обратном порядке их создания. Поле, конструктор которого выбросил исключение, считается не сконструированным, поэтому его деструктор не вызывается. Деструктор самого объекта класса также не вызывается, потому что объект не завершил конструирование.

Исторический контекст

RAII использует гарантированное уничтожение автоматических объектов и их подобъектов при выходе из области видимости. Это позволяет связывать ресурс с временем жизни объекта: конструктор получает ресурс, а деструктор освобождает его.

Такой механизм особенно важен при исключениях. Без него каждый частично выполненный конструктор должен был бы вручную откатывать уже полученные ресурсы, что легко приводит к пропускам освобождения и утечкам.

Постановка проблемы

Класс может последовательно создавать несколько ресурсов: например, открывать файл, подключение и временный буфер. Если создание второго ресурса завершается исключением, первый ресурс уже нельзя оставлять без владельца.

Неверное решение — хранить первый ресурс в сыром указателе или дескрипторе и рассчитывать на деструктор всего класса. Деструктор класса не будет вызван для объекта, конструирование которого прервано, поэтому такой ресурс может утечь.

Подробное решение

Сначала полностью конструируются базовые классы, затем поля в порядке их объявления в классе, а не в порядке списка инициализации конструктора. Если конструктор очередного поля выбросил исключение, это поле не считается созданным.

После этого C++ разрушает уже созданные поля в порядке, обратном их конструированию. Затем уничтожаются полностью созданные базовые подобъекты также в обратном порядке. Тело конструктора при исключении из списка инициализации не выполняется полностью, а деструктор внешнего класса не вызывается.

Поэтому каждый ресурс должен находиться в подобъекте, чей деструктор способен освободить его самостоятельно. Обычно для этого используют std::unique_ptr, контейнеры, строки или собственный RAII-тип с корректным деструктором.

Порядок объявления полей становится частью корректности класса. Если один ресурс логически зависит от другого, зависимый ресурс следует объявлять позже: тогда при исключении или обычном уничтожении он будет освобождён раньше ресурса, от которого зависит.

Если ресурс уже получен в теле конструктора и хранится отдельно от RAII-объекта, исключение между получением ресурса и созданием владельца может вызвать утечку. Безопаснее сначала создать локальный RAII-владелец, а затем передать ресурс в поле, либо сразу хранить его в поле подходящего типа.

Ситуация из практики

Сервисный класс открывает системный дескриптор в первом поле и создаёт сетевой буфер во втором. Выделение буфера может завершиться исключением.

Рассмотрены два варианта:

  • Хранить дескриптор в сыром целочисленном поле и закрывать его в деструкторе класса. Это просто, но небезопасно: при исключении из конструктора деструктор класса не запустится.
  • Обернуть дескриптор в отдельный RAII-тип с деструктором, закрывающим дескриптор. Это требует небольшого вспомогательного класса, зато корректно работает при частичном конструировании.

Выбран второй вариант. Если буфер не создаётся, уже сконструированный владелец дескриптора автоматически закрывает его при раскрутке стека. В результате утечка не зависит от того, на каком шаге конструктора произошёл сбой.

Что кандидаты часто упускают

  1. Вызывается ли деструктор поля, если его конструктор выбросил исключение?

Нет. Объект поля не был полностью сконструирован, поэтому его деструктор не вызывается. Однако его уже полностью созданные подобъекты, например базовые классы и предыдущие поля, будут корректно уничтожены.

  1. В каком порядке уничтожаются поля, если список инициализации записан в другом порядке?

Порядок записи списка не меняет правила языка. Поля создаются в порядке объявления в классе и уничтожаются в обратном порядке. Компилятор обычно предупреждает о несовпадении этих порядков, поскольку такая ошибка часто маскирует зависимость между полями.

  1. Что произойдёт с ресурсом, полученным до исключения, если он хранится в сыром указателе?

Сам по себе сырой указатель ресурс не освобождает. Если деструктор внешнего класса не будет вызван из-за исключения в конструкторе, ресурс останется потерянным, если только обработка исключения не выполнит освобождение вручную. Надёжнее сразу передать ресурс во временный или полевой RAII-владелец, чтобы его очистка была связана с разрушением уже сконструированного подобъекта.