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

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

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

Ответ.

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

C++ изначально проектировался с акцентом на производительность, поэтому управление ресурсами (память, файлы, потоки, сокеты) чаще происходит вручную. При возникновении исключения требуется очищать захваченные ресурсы. Stack unwinding (разматывание стека) — механизм, используемый C++ для корректного завершения работы функций во время выброса исключения.

Проблема:

При выбрасывании исключения управление немедленно переходит к блокам catch, а промежуточные функции "разматываются": их деструкторы вызываются, но явные вызовы освобождающих функций могут быть пропущены (например, если не используются объекты, автоматически освобождающие ресурсы).

Решение:

В C++ освобождение ресурсов следует поручать деструкторам, а не вызывать освобождающие функции вручную. Шаблон RAII (Resource Acquisition Is Initialization) — однозначный способ сделать освобождение ресурса автоматическим. При stack unwinding вызовется деструктор, и ресурс освободится независимо от пути выхода из функции.

Пример кода:

#include <fstream> #include <stdexcept> void readFile(const std::string& filename) { std::ifstream file(filename); // Откроется и корректно закроется даже при исключении if (!file.is_open()) { throw std::runtime_error("File cannot be opened"); } // ... читаем файл } // file закроется даже при исключении

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

  • Stack unwinding — стандартный механизм уничтожения объектов при выбросе исключения.
  • Освобождение ресурсов всегда производите в деструкторах.
  • Используйте RAII или стандартные классы (например, умные указатели).

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

Если в коде есть delete ptr; в блоке catch, этого достаточно для очистки памяти?

Нет, если между выделением памяти и блоком catch появится исключение, память может не быть очищена. Лучше использовать std::unique_ptr или писать delete в деструкторе.

Пример кода:

void foo() { int* data = new int[10]; // ... throw std::runtime_error("fail"); delete[] data; // не вызовется при исключении }

Может ли stack unwinding пропустить вызов деструктора для объекта, размещённого на стеке?

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

Можно ли использовать goto или longjmp для выхода из try-блока и рассчитывать на вызов деструкторов?

Нет. C++ гарантирует вызов деструкторов только при stack unwinding из-за исключения, а не из-за некорректного управления потоком (goto, setjmp/longjmp).

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

  • Очищать ресурсы вручную внутри try-catch, игнорируя деструкторы
  • Использовать сырые указатели вместо RAII или стандартных классов
  • Абстрагировать обработку исключений так, что ресурсы не освобождаются (например, setjmp/longjmp)

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

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

Программист выделяет память с помощью new, обрабатывает исключения, освобождая память в блоке catch, но забывает о других путях выхода из функции.

Плюсы:

  • По-началу кажется просто и "прозрачно"

Минусы:

  • Если исключение брошено не там, где ожидалось, возникает утечка памяти
  • Сложно тестировать и сопровождать

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

Используется std::unique_ptr и RAII-классы для всех ресурсов, освобождение не зависит от try/catch.

Плюсы:

  • Нет утечек ресурсов
  • Логика обработки ошибок становится проще

Минусы:

  • Требует большего понимания стандартной библиотеки и идиом языка