В каких случаях деструкторы объектов C++ могут не вызваться при возникновении исключения?

Ответ

Деструкторы автоматических (стековых) объектов гарантированно вызываются при выходе из их области видимости в процессе раскрутки стека (stack unwinding). Однако есть сценарии, где эта гарантия нарушается:

  1. Исключение, покинувшее функцию main() без перехвата. Программа завершается вызовом std::terminate(), и раскрутка стека не гарантируется.

    int main() {
        ResourceHandle res; // Деструктор может не вызваться
        throw std::runtime_error("Uncaught!");
        // Завершение через std::terminate()
    }
  2. Исключение, выброшенное в деструкторе во время активной обработки другого исключения. Это приводит к немедленному вызову std::terminate() (правило: деструкторы не должны выбрасывать исключения).

    struct Bad {
        ~Bad() noexcept(false) { throw 42; } // Опасность!
    };
    int main() {
        try {
            Bad obj;
            throw std::runtime_error("First"); // Начало раскрутки стека
        } catch(...) { } // Сюда не попадем, вызовется terminate()
    }
  3. Некорректное использование низкоуровневых средств управления потоком выполнения, обходящих механизм исключений C++.

    • Функции longjmp()/setjmp() из языка C. Переход longjmp() через кадры стека с объектами C++ ведет к неопределенному поведению.
    • Прямой вызов std::exit() или std::abort(). Раскрутка стека не производится.
  4. Утечка указателя на динамически выделенную память. Если исключение возникает до вызова delete для сырого указателя, деструктор объекта в куче не вызовется.

    void risky() {
        MyClass* ptr = new MyClass;
        someFunctionThatMayThrow(); // Может выбросить исключение
        delete ptr; // Эта строка может не выполниться
    }

    Решение: Использовать умные указатели (std::unique_ptr, std::shared_ptr), чьи деструкторы вызовут delete даже при исключении (паттерн RAII).

Ответ 18+ 🔞

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

Вот смотри, первый номер программы — исключение, которое улетело из main() и никто его не поймал. Представь: ты в main() создал какую-нибудь ResourceHandle, выбросил исключение, а ловить его некому. Программа говорит: «Всё, пиздец», вызывает std::terminate(), и всё — раскрутки стека может и не быть. Деструктор твоего объекта может и не дернуться. Сам от себя охуеешь потом, когда ресурсы не освободятся.

int main() {
    ResourceHandle res; // Деструктор может не вызваться
    throw std::runtime_error("Uncaught!");
    // Завершение через std::terminate()
}

Дальше — второй пиздец, классический. Исключение вылетает из деструктора, когда уже летит другое исключение. Это, блядь, как подлить бензину в костёр. Правило простое: деструкторы не должны кидать исключения, иначе std::terminate() вызывается немедленно, без лишних разговоров. Терпения ноль ебать у компилятора на этот счёт.

struct Bad {
    ~Bad() noexcept(false) { throw 42; } // Опасность! Прям манда с ушами.
};
int main() {
    try {
        Bad obj;
        throw std::runtime_error("First"); // Начало раскрутки стека
    } catch(...) { } // Сюда не попадем, вызовется terminate()
}

Третий пункт — это для любителей острых ощущений, которые лезут в низкоуровневое управление выполнением. Функции longjmp() и setjmp() из Си — это, блядь, чистой воды русская рулетка для объектов C++. Если прыгнешь через кадры стека, где живут твои объекты, получишь неопределённое поведение, а деструкторы — хуй с горы. Ну и вызов std::exit() или std::abort() — это просто приказ на расстрел без права на последнее слово. Раскрутки стека не будет, никаких деструкторов.

И, наконец, четвёртый акт этой драмы — утечка памяти из-за исключения. Классика жанра, ебать копать. Создал объект через new, потом вызвал функцию, которая может выбросить, и delete не сработает. Объект в куче так и останется висеть, как грустный хуй в пальто.

void risky() {
    MyClass* ptr = new MyClass;
    someFunctionThatMayThrow(); // Может выбросить исключение
    delete ptr; // Эта строка может не выполниться
}

Но решение-то, блядь, простое и элегантное, как удар вилкой в глаз. Умные указатели, std::unique_ptr или std::shared_ptr. Их деструкторы вызовут delete за тебя, даже если всё вокруг летит в тартарары. Это и есть тот самый RAII, который спасает от этих ебанутых сценариев. Просто используй их, и живи спокойно.