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

Когда могут не вызваться деструкторы при исключении?

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

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

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

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

Когда деструкторы не вызываются при исключении

Это важный вопрос о надёжности C++ приложений. Stack unwinding с вызовом деструкторов — это основа RAII и управления ресурсами в C++. Но есть ситуации, когда вызов гарантирован НЕ происходит.

1. Исключение в конструкторе

Проблема: Если конструктор выбросит исключение, деструктор вообще не вызовется, потому что объект не был полностью построен.

class Resource {
public:
    int* buffer;
    
    Resource(size_t size) {
        buffer = new int[size];
        if (size > 1000000) {
            throw std::runtime_error("Size too large");
            // Деструктор НИКОГДА не вызовется!
            // buffer остаётся висящим указателем
        }
    }
    
    ~Resource() {
        delete[] buffer;
    }
};

int main() {
    try {
        Resource r(2000000);  // Выбросит исключение
    } catch (...) {
        // Утечка памяти! Деструктор не вызван
    }
}

Решение — двухэтапная инициализация:

class Resource {
    int* buffer;
    bool initialized;
    
public:
    Resource() : buffer(nullptr), initialized(false) {}
    
    void init(size_t size) {  // Отдельный метод инициализации
        buffer = new int[size];
        if (size > 1000000) {
            delete[] buffer;
            throw std::runtime_error("Size too large");
        }
        initialized = true;
    }
    
    ~Resource() {
        if (initialized && buffer) {
            delete[] buffer;
        }
    }
};

// Лучше: используй smart pointers
class ResourceModern {
    std::unique_ptr<int[]> buffer;
    
public:
    ResourceModern(size_t size) : buffer(std::make_unique<int[]>(size)) {
        if (size > 1000000) {
            throw std::runtime_error("Size too large");
            // unique_ptr деструктор БУДЕТ вызван при unwinding
        }
    }
};

2. Исключение в деструкторе

Проблема: Если деструктор выбросит исключение во время stack unwinding'а, программа либо вызовет std::terminate(), либо потеряет исходное исключение.

class Database {
public:
    ~Database() {
        close();  // Что если close() выбросит исключение?
    }
    
    void close() {
        // Попытка закрыть соединение
        if (connection_broken) {
            throw std::runtime_error("Connection error");
        }
    }
};

int main() {
    try {
        Database db;
        // Во время выхода из scope, деструктор вызывает close(),
        // который выбрасывает исключение
        // → std::terminate() вызывается, программа падает!
    } catch (...) {}
}

Правило: деструкторы НИКОГДА не должны выбрасывать исключения!

class DatabaseSafe {
public:
    ~Database() noexcept {  // noexcept гарантирует отсутствие исключений
        try {
            close();
        } catch (const std::exception& e) {
            // Логируем ошибку, но не выбрасываем
            std::cerr << "Error closing database: " << e.what() << std::endl;
        }
    }
};

3. Исключение до построения объекта

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

class MyClass {
public:
    std::unique_ptr<Resource> res1;
    std::unique_ptr<Resource> res2;
    std::string name;
    
    MyClass(const std::string& n) 
        : res1(std::make_unique<Resource>(100)),
          res2(std::make_unique<Resource>(throwIfLarge())),  // Может выбросить
          name(n) {
        // res1 уже создан и будет успешно удалён
        // res2 не создан, так что его деструктор не вызовется
        // name уже инициализирован и удалится
        // Но порядок удаления гарантирован в обратном порядке инициализации
    }
};

С modern C++ и smart pointers это обычно безопасно, но важно помнить о порядке инициализации.

4. Вызов std::exit() или std::abort()

Проблема: Глобальные объекты и объекты на stack не удаляются.

class Logger {
public:
    ~Logger() {
        std::cout << "Logger destroyed" << std::endl;
    }
};

Logger global_logger;

int main() {
    std::exit(0);  // Деструктор global_logger МОЖЕТ не вызваться
    // (зависит от реализации, но обычно вызовется)
}

Правило: std::exit() вызывает деструкторы глобальных объектов, но std::abort() — НЕ вызывает. Избегай std::abort() в коде.

5. Исключение в глобальных конструкторах

Проблема: Если конструктор глобального объекта выбросит исключение, программа прекращает работу ещё до вызова main().

struct GlobalResource {
    GlobalResource() {
        throw std::runtime_error("Can't initialize global resource");
        // Нет обработки этого исключения!
        // Программа просто умирает
    }
};

GlobalResource gr;  // Исключение выбросится при загрузке библиотеки

int main() {
    // Никогда не будет достигнут
}

Решение: Используй lazy initialization для глобальных ресурсов.

6. Исключение в signal handler'е

Проблема: Исключения НЕ могут быть выброшены из signal handler'ов!

void signal_handler(int sig) {
    throw std::runtime_error("Signal received!");  // UNDEFINED BEHAVIOR!
}

int main() {
    std::signal(SIGINT, signal_handler);
}

7. Исключение при выполнении с optimization уровнем -fno-exceptions

Проблема: Если компилируешь с -fno-exceptions, обработка исключений отключена.

g++ -fno-exceptions program.cpp  # Исключения не работают!

Практические рекомендации

// ✅ ПРАВИЛЬНО - используй RAII
class ConnectionPool {
    std::vector<std::unique_ptr<Connection>> connections;
    std::mutex m;
    
public:
    ~ConnectionPool() noexcept {  // noexcept!
        // unique_ptr автоматически удалит connections
    }
};

// ✅ ПРАВИЛЬНО - обработай исключения в деструкторе
~MyClass() noexcept {
    try {
        cleanup();
    } catch (const std::exception& e) {
        LOG_ERROR("Cleanup failed: ", e.what());
    }
}

// ❌ НЕПРАВИЛЬНО - выброс из деструктора
~MyClass() {  // БЕЗ noexcept!
    throw std::runtime_error("Error!");  // ОПАСНО!
}

Чек-лист для production кода

  • Все деструкторы помечены noexcept
  • Деструкторы не выбрасывают исключения
  • Используются smart pointers (unique_ptr, shared_ptr)
  • Критичные ресурсы имеют fail-safe cleanup
  • Глобальные объекты инициализируются безопасно
  • Исключения в конструкторах обработаны