ПрограммированиеC++ Backend разработчик

Опишите различия между stack unwinding и разрушением объектов при обработке исключений в C++. Как гарантировать корректное освобождение ресурсов при возникновении исключения?

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

Ответ.

В C++ обработка исключений сопровождается механизмом stack unwinding — автоматического разрушения (деструкторизации) локальных объектов в стеке вызовов, созданных до возникновения исключения. Это явление важно для корректного освобождения ресурсов.

История вопроса

Изначально, при проектировании C++, недостаёт built-in сборщика мусора, и задача освобождения ресурсов (памяти, файлов, сокетов) ложится на программиста. Обработка исключений должна была обеспечивать корректное высвобождение при откате кода из-за ошибки.

Проблема

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

Решение

C++ реализует stack unwinding, когда при выбрасывании исключения все объекты, размещённые на стеке в области видимости до throw, уничтожаются в обратном порядке создания. Их деструкторы вызываются автоматически.

Классический способ постоянно освобождать ресурсы — применять паттерн RAII (Resource Acquisition Is Initialization): все ресурсы "завёрнуты" в объекты, которые при разрушении освобождают владеемый ими ресурс.

Пример кода:

#include <iostream> #include <stdexcept> struct FileHandle { FILE* file; FileHandle(const char* path) { file = fopen(path, "r"); if (!file) throw std::runtime_error("Cannot open file"); } ~FileHandle() { if (file) fclose(file); } }; void processFile(const char* path) { FileHandle fh(path); // Откатит, если будет throw // ... работа с файлом throw std::runtime_error("Some error"); // fclose автоматически вызовется }

Ключевые особенности:

  • Stack unwinding автоматически вызывает деструкторы всех объектов в стеке.
  • RAII гарантирует освобождение ресурсов независимо от возникновения исключения.
  • Самостоятельное освобождение ресурсов в catch редко нужно: деструкторы делают это лучше.

Вопросы с подвохом.

Почему нельзя просто воспользоваться try-catch и вручную вызвать delete или fclose в блоке catch, чтобы избежать утечек?

Ответ:

Это неудобно и ненадёжно: легко забыть закрыть ресурс, особенно при нескольких точках выхода или вложенных ресурсах. Деструкторы вызываются даже если исключение "пролетает" через функцию, catch для этого не нужен.

Будут ли уничтожены объекты в куче (heap), если они не "завёрнуты" в объекты на стеке при stack unwinding?

Ответ:

Нет. Stack unwinding вызывает деструкторы только для объектов размещённых на стеке. Чтобы heap-объекты уничтожались корректно, ими должен владеть объект в стеке (например, через smart pointers).

Что будет, если деструктор при stack unwinding сам выбрасывает исключение?

Ответ:

Если в процессе stack unwinding во время разрушения какого-либо объекта будет выброшено второе исключение, программа завершится вызовом std::terminate(). Никогда не выбрасывайте исключения из деструкторов!

Типовые ошибки и анти-паттерны

  • Не освобождать ресурсы в деструкторах, полагаясь лишь на ручное управление в catch.
  • Выбрасывать исключения из деструкторов.
  • Не использовать умные указатели для управления heap-ресурсами.
  • Забывать о вложенных ресурсах (например, файл внутри структуры).

Пример из жизни

Негативный кейс

Разработчик вручную закрывает файлы и освобождает память через catch-блоки без применения RAII.

Плюсы:

  • Явно управляет ресурсами.

Минусы:

  • В случае выхода через throw до catch утечки не избежать.
  • Поддерживать и модифицировать код сложно, легко забыть освобождение.

Позитивный кейс

Файлы и ресурсы оборачиваются в классы-обёртки RAII. Итог: освобождение происходит в деструкторах, независимо от throw/catch.

Плюсы:

  • Надёжность освобождения ресурсов.
  • Меньше кода, легко сопровождать.

Минусы:

  • Нужно уметь писать RAII-обёртки.
  • Добавление новых видов ресурсов требует новых классов.