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

Как избежать Double Exception?

2.0 Middle🔥 101 комментариев
#Исключения и обработка ошибок

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

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

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

Как избежать Double Exception

Double Exception — это критическая ошибка в C++, которая возникает, когда исключение выбрасывается во время обработки другого исключения. Это приводит к немедленному завершению программы (вызов std::terminate()).

Что такое Double Exception?

Double Exception происходит, когда:

  1. Основное исключение находится в процессе обработки
  2. Во время обработки (в блоке catch или деструкторе) возникает новое исключение
  3. C++ Runtime не может обработать ситуацию и вызывает std::terminate()
#include <iostream>
using namespace std;

class BadDestructor {
public:
    ~BadDestructor() {
        throw runtime_error("Error in destructor!"); // DANGER!
    }
};

int main() {
    try {
        BadDestructor obj;
        throw logic_error("First error");
    } catch (const exception& e) {
        cout << "Caught: " << e.what() << endl;
        // Во время раскручивания стека вызывается ~BadDestructor()
        // Он выбрасывает исключение -> DOUBLE EXCEPTION -> terminate()
    }
    return 0;
}

Главное правило: НИКОГДА не выбрасывайте в деструкторах

Деструкторы вызываются автоматически, в том числе во время раскручивания стека при исключении. Если деструктор выбросит исключение, возникнет double exception.

// ❌ ПЛОХО: исключение в деструкторе
class Resource {
public:
    ~Resource() {
        if (!cleanup()) {
            throw runtime_error("Cleanup failed!");
        }
    }
private:
    bool cleanup() { /* может вернуть false */ }
};

// ✅ ХОРОШО: никогда не выбрасываем
class Resource {
public:
    ~Resource() noexcept {
        if (!cleanup()) {
            // Логируем ошибку, но не выбрасываем
            cerr << "Warning: cleanup failed" << endl;
        }
    }
private:
    bool cleanup() { /* может вернуть false */ }
};

Почему noexcept в деструкторах?

С C++11 деструкторы по умолчанию помечены как noexcept. Если деструктор попытается выбросить исключение, это немедленно вызовет std::terminate().

class FileWrapper {
public:
    // Деструктор неявно noexcept
    ~FileWrapper() {
        // НИКОГДА не выбрасываем исключения здесь!
        file.close(); // должен быть noexcept
    }
private:
    std::ofstream file;
};

Стратегия обработки ошибок в деструкторах

1. Используй try-catch внутри деструктора

class DatabaseConnection {
public:
    ~DatabaseConnection() noexcept {
        try {
            disconnect();
        } catch (const exception& e) {
            // Логируем, но не выбрасываем
            cerr << "Error closing connection: " << e.what() << endl;
        }
    }
private:
    void disconnect() { /* может выбросить */ }
};

2. Используй функцию-помощник с возвращаемым кодом ошибки

class Network {
public:
    ~Network() noexcept {
        int result = closeSocket();
        if (result != 0) {
            // Логируем ошибку
            log("Socket close failed: " + to_string(result));
        }
    }
private:
    int closeSocket() noexcept {
        // Возвращаем код ошибки вместо исключения
        try { /* ... */ return 0; }
        catch (...) { return -1; }
    }
};

Обработка исключений в блоках catch

Избегай выбрасывания новых исключений в catch-блоке без необходимости:

// ❌ ПЛОХО: теряем оригинальное исключение
try {
    riskyOperation();
} catch (const exception& e) {
    throw runtime_error("Operation failed"); // теряем информацию о e
}

// ✅ ХОРОШО: переформатируем с context
try {
    riskyOperation();
} catch (const exception& e) {
    throw runtime_error(string("Operation failed: ") + e.what());
}

// ✅ ХОРОШО: используем nested exceptions (C++11)
try {
    riskyOperation();
} catch (const exception& e) {
    try {
        throw runtime_error("Operation failed");
    } catch (...) {
        throw_with_nested(e);
    }
}

Практический пример защиты от Double Exception

#include <iostream>
#include <memory>
using namespace std;

class SafeResource {
public:
    SafeResource() { cout << "Resource acquired" << endl; }
    
    ~SafeResource() noexcept {
        // Гарантировано, что не выбросим исключение
        cleanup();
    }
    
    void cleanup() noexcept {
        // Логируем ошибки, но не выбрасываем
        try {
            // Опасная операция
            if (false) { throw runtime_error("Cleanup error"); }
            cout << "Resource released" << endl;
        } catch (const exception& e) {
            cerr << "Cleanup warning: " << e.what() << endl;
        }
    }
};

int main() {
    try {
        SafeResource res;
        throw logic_error("Main error");
    } catch (const exception& e) {
        cout << "Caught: " << e.what() << endl;
        // При выходе вызовется ~SafeResource(), но она не выбросит исключение
    }
    cout << "Program continued safely" << endl;
    return 0;
}

Чеклист безопасности

  1. Всегда помечай деструкторы как noexcept (явно или неявно)
  2. Никогда не выбрасывай исключения в деструкторах
  3. Оборни опасный код в деструкторе в try-catch
  4. Используй RAII принцип для гарантии вызова деструкторов
  5. Логируй ошибки вместо выброса исключений
  6. Тестируй обработку исключений с использованием специальных флагов

Вывод: Double Exception — это серьёзная проблема, которая приводит к аварийному завершению программы. Главное правило: деструкторы должны быть исключение-безопасными. Используй RAII, try-catch в деструкторах и логирование вместо выброса исключений.