← Назад к вопросам

На каком этапе запретится использование throw в деструкторе

1.7 Middle🔥 91 комментариев
#Исключения и обработка ошибок#Язык C++

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI29 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

На каком этапе запретится использование throw в деструкторе

Использование throw в деструкторе — это опасная практика, и компилятор имеет механизмы контроля над этим поведением в зависимости от версии стандарта C++ и конфигурации.

Основная проблема

Выброс исключения из деструктора приводит к undefined behavior в C++, особенно если деструктор вызывается во время обработки другого исключения (stack unwinding):

class Resource {
public:
    ~Resource() {
        if (some_error) {
            throw std::runtime_error("Cleanup failed!");  // ОПАСНО!
        }
    }
};

int main() {
    try {
        Resource res;
        throw std::logic_error("Something went wrong");
        // Во время раскрутки стека вызовется ~Resource()
        // Если она выбросит исключение — процесс прервётся!
    } catch (const std::exception& e) {
        std::cout << "Caught: " << e.what() << "\n";
    }
    return 0;
}
// Вывод: вызов std::terminate(), программа аварийно завершается

C++11 и позже: noexcept по умолчанию

С C++11 деструкторы и operator= по умолчанию объявлены как noexcept, что означает "этот деструктор НЕ выбрасывает исключения":

class MyClass {
    int* ptr;
public:
    ~MyClass() noexcept(true) {  // Явно noexcept
        delete ptr;
    }
};

Если вы попытаетесь выбросить исключение из debructor, помеченного noexcept (или просто из деструктора без явного noexcept(false)), компилятор вызовет std::terminate():

class BadClass {
public:
    ~BadClass() {  // Неявно noexcept(true) с C++11
        throw std::runtime_error("Error in destructor");
        // Это вызовет std::terminate()!
    }
};

int main() {
    BadClass obj;
    // При выходе из scope вызовется ~BadClass()
    // и программа завершится с ошибкой
    return 0;
}

Этапы жизни правила

C++98/03: Деструкторы по умолчанию не имели спецификации исключений. Выброс исключения из деструктора считался ошибкой программиста, но не запрещался на уровне компилятора.

C++11: Вводится noexcept спецификация. Деструкторы, виртуальные методы перемещения и operator= автоматически помечаются как noexcept по умолчанию. Нарушение этого контракта вызывает std::terminate().

C++17: Правила ужесточаются — спецификация исключений для деструкторов становится более строгой и проверяется компилятором.

C++20 и позже: Дополнительные улучшения и оптимизации на основе информации о noexcept.

Явное разрешение throw в деструкторе

Если вам действительно нужно выбросить исключение из деструктора, вы можете явно указать noexcept(false). Это крайне нежелательно, но возможно:

class DangerousClass {
public:
    ~DangerousClass() noexcept(false) {  // Явное разрешение!
        throw std::runtime_error("Destructor throws!");
    }
};

int main() {
    try {
        DangerousClass obj;
    } catch (const std::exception& e) {
        std::cout << "Caught from destructor: " << e.what() << "\n";
    }
    return 0;
}

Однако, если это произойдёт во время раскрутки стека от другого исключения, вызовется std::terminate():

int main() {
    try {
        try {
            DangerousClass obj;
            throw std::logic_error("First error");
            // Во время раскрутки стека вызовется ~DangerousClass()
            // Она выбросит исключение, но мы уже обрабатываем исключение
            // → std::terminate()!
        } catch (...) {
            throw;
        }
    } catch (const std::exception& e) {
        std::cout << e.what() << "\n";
    }
    return 0;
}

Правильный подход — обработка ошибок в деструкторе

Вариант 1: Логирование без выброса

class Resource {
    std::string name;
public:
    ~Resource() noexcept {
        try {
            cleanup();
        } catch (const std::exception& e) {
            // Логируем, но НЕ выбрасываем
            std::cerr << "Cleanup failed for " << name << ": " 
                      << e.what() << "\n";
        }
    }
    
private:
    void cleanup();
};

Вариант 2: Явная обработка до уничтожения

class Resource {
    std::string name;
public:
    void close() {  // Явный метод для закрытия с ошибками
        if (!is_open) return;
        // Может выбросить исключение
        cleanup();
        is_open = false;
    }
    
    ~Resource() noexcept {
        try {
            if (is_open) close();
        } catch (...) {
            // Обработали, но не выбрасываем
        }
    }
    
private:
    bool is_open = true;
    void cleanup();
};

int main() {
    Resource res;
    try {
        res.close();  // Ошибки обрабатываются здесь
    } catch (const std::exception& e) {
        std::cerr << "Error: " << e.what() << "\n";
    }
    // ~Resource() будет вызван безопасно
    return 0;
}

Резюме

  • C++11+: Деструкторы по умолчанию noexcept(true). Выброс исключения вызывает std::terminate()
  • Запрет на этапе runtime: Если вы нарушите контракт noexcept, программа аварийно завершится
  • Нарушение во время раскрутки стека: Гарантированно вызовет std::terminate() независимо от noexcept(false)
  • Лучшая практика: Обрабатывайте ошибки в деструкторе без выброса исключений; выполняйте важные операции с ошибками в явных методах до уничтожения объекта
На каком этапе запретится использование throw в деструкторе | PrepBro