Зачем компилятор неявно помечает деструкторы как noexcept?

Ответ

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

Основная причина: Предотвратить катастрофическое завершение программы. Если во время раскрутки стека (stack unwinding), вызванной другим исключением, деструктор выбрасывает новое исключение, программа немедленно вызывает std::terminate(). Неявное noexcept делает такое поведение предсказуемым и предотвращает неопределённое состояние.

#include <iostream>

struct Resource {
    ~Resource() { std::cout << "Resource cleanedn"; }
};

struct Safe {
    // Компилятор сгенерирует: ~Safe() noexcept = default;
    Resource res;
};

struct Dangerous {
    ~Dangerous() noexcept(false) { // Явно разрешаем исключения
        throw std::runtime_error("Exception in dtor!");
    }
};

int main() {
    try {
        Safe s; // Деструктор noexcept
        throw 42; // Начало раскрутки стека
        // Деструктор 's' будет вызван, исключений не бросит.
    } catch (...) {}

    try {
        Dangerous d;
        throw 42; // Раскрутка стека начнется
        // Вызов ~Dangerous() бросит исключение ВО ВРЕМЯ раскрутки -> std::terminate()!
    } catch (...) {
        // Сюда управление не дойдет
    }
    return 0;
}

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

Ответ 18+ 🔞

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

Вот чтобы такого пиздеца не было, умные дядьки в стандарте C++11 и порешали: а давайте сделаем так, чтобы компилятор по умолчанию генерировал деструкторы с noexcept. То есть, если ты сам явно не написал noexcept(false), то твой деструктор считается безопасным и обещает не кидать никаких сюрпризов. Это, блядь, как гарантия, что во время пожара (раскрутки стека) пожарные (деструкторы) не начнут сами подливать бензин.

#include <iostream>

struct Resource {
    ~Resource() { std::cout << "Resource cleanedn"; }
};

struct Safe {
    // Компилятор сгенерирует: ~Safe() noexcept = default;
    Resource res;
};

struct Dangerous {
    ~Dangerous() noexcept(false) { // Явно разрешаем исключения
        throw std::runtime_error("Exception in dtor!");
    }
};

int main() {
    try {
        Safe s; // Деструктор noexcept
        throw 42; // Начало раскрутки стека
        // Деструктор 's' будет вызван, исключений не бросит.
    } catch (...) {}

    try {
        Dangerous d;
        throw 42; // Раскрутка стека начнется
        // Вызов ~Dangerous() бросит исключение ВО ВРЕМЯ раскрутки -> std::terminate()!
    } catch (...) {
        // Сюда управление не дойдет
    }
    return 0;
}

Смотри, в чём прикол. В первом случае с Safe — всё окей. Вылетело исключение 42, началась раскрутка, вызвался деструктор s, он отработал тихо, как мышь, и управление перешло в блок catch. А вот во втором случае, с Dangerous — это просто ядрёна вошь. Ты создал объект d, потом кинул исключение, пошла раскрутка... и тут вызывается деструктор ~Dangerous(), который сам по себе, пидарас шерстяной, выкидывает ещё одно исключение! Два исключения в одном потоке — это как гомосеки налетели, программа не знает, за какое хвататься, и просто вызывает std::terminate(). Всё, конец, кирдык. Блок catch уже не спасёт, туда управление никогда не дойдёт.

Поэтому главное практическое правило, которое нужно выжечь себе в подкорке: деструкторы должны быть noexcept. Точка. Если тебе в деструкторе нужно сделать что-то опасное, что может сбоить — закрыть файл, разорвать сетевое соединение, отправить последнее прощальное письмо — ты обязан эту ошибку заглотить внутри самого деструктора. Залогировать, проигнорировать, послать в /dev/null — делай что угодно, но не давай исключению вырваться наружу. Потому что если оно вырвется — это будет волнение ебать, и доверия к твоему коду будет ноль ебать. Деструктор — это последний рубеж, место, где нужно не геройствовать, а гарантированно прибраться, даже если всё вокруг уже горит синим пламенем.