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

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

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

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

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

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

noexcept в деструкторах: безопасность и производительность

Деструкторы по умолчанию помечаются как noexcept — они не должны выбрасывать исключения. Это критическое правило C++.

Почему это критично

1. Undefined Behavior во время развертывания стека

Когда выбрасывается исключение, начинается stack unwinding — вызываются деструкторы всех объектов на пути:

void process() {
    MyObject obj1;
    MyObject obj2;
    
    throwException();  // obj2, затем obj1 будут уничтожены
}

Если деструктор выбросит исключение во время развертывания, то:

  • Одновременно активны два исключения
  • C++ не может их обработать
  • Вызывается std::terminate() — программа падает
class BadDestructor {
public:
    ~BadDestructor() {
        throw std::runtime_error("Ошибка в деструкторе!");
    }
};

int main() {
    try {
        BadDestructor obj;
        throw std::logic_error("Первое исключение");
        // При выходе вызовется деструктор
        // Он выбросит ВТОРОЕ исключение
        // std::terminate() — программа упадёт!
    } catch (...) {}
    return 0;
}

2. Деструкторы помечаются noexcept автоматически

Компилятор помечает деструктор как noexcept(true) по умолчанию, если:

  • Не содержит явного throw()
  • Все базовые классы имеют noexcept деструкторы
  • Все члены данных имеют noexcept деструкторы
class Safe {
public:
    ~Safe() = default;  // Автоматически noexcept
};

class Unsafe {
public:
    ~Unsafe() noexcept(false) {  // Явно non-throwing
        throw std::runtime_error("Ошибка");
    }
};

Практические последствия

Проблема с контейнерами

Стандартные контейнеры (vector, map и т.д.) полагаются на noexcept деструкторы:

std::vector<Unsafe> vec;
vec.push_back(Unsafe());  // Если вектор переаллоцируется,
                         // старые объекты уничтожаются
                         // Их деструкторы могут выбросить исключение!

Move семантика зависит от noexcept

struct WithThrowingDestructor {
    WithThrowingDestructor() {}
    WithThrowingDestructor(WithThrowingDestructor&&) {}
    ~WithThrowingDestructor() noexcept(false) {
        throw std::runtime_error("Нет!");
    }
};

int main() {
    std::vector<WithThrowingDestructor> vec;
    vec.reserve(10);
    vec.emplace_back();  // Может быть опасно
    
    // std::vector<T> может копировать вместо перемещения,
    // если move конструктор не noexcept!
}

Пример безопасного кода

class Resource {
private:
    int* ptr;
    size_t size;

public:
    Resource(size_t sz) : size(sz), ptr(new int[sz]) {}
    
    // Деструктор всегда noexcept — никогда не выбрасывает
    ~Resource() noexcept {
        delete[] ptr;  // Не может выбросить исключение
    }
    
    // Move конструктор noexcept — std::vector поймет
    Resource(Resource&& other) noexcept
        : ptr(other.ptr), size(other.size) {
        other.ptr = nullptr;
        other.size = 0;
    }
};

int main() {
    try {
        std::vector<Resource> vec;
        vec.push_back(Resource(1000));
        vec.push_back(Resource(2000));
        
        throw std::runtime_error("Ошибка!");
        // При развертывании стека деструкторы вызовутся безопасно
    } catch (const std::exception& e) {
        std::cout << e.what();
    }
    return 0;
}

Правило: никогда не выбрасывай из деструктора

Правило 1: Все деструкторы должны быть noexcept

class Good {
public:
    ~Good() noexcept {  // Явно и безопасно
        // Очистка ресурсов
        // Никаких throw!
    }
};

Правило 2: Если нужна обработка ошибок в деструкторе — используй логирование, не исключения

class Database {
public:
    ~Database() noexcept {
        if (connection) {
            try {
                connection->close();
            } catch (const std::exception& e) {
                // Логируем, но не выбрасываем!
                std::cerr << "Error closing DB: " << e.what();
            }
        }
    }
};

Заключение

Деструкторы помечаются noexcept потому что:

  1. Безопасность — исключение из деструктора вызовет std::terminate()
  2. Гарантия — компилятор берет на себя ответственность
  3. Оптимизация — контейнеры и move семантика полагаются на этот контракт

Никогда не выбрасывай исключения из деструкторов.

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