← Назад к вопросам
Зачем компилятор помечает деструкторы как 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 потому что:
- Безопасность — исключение из деструктора вызовет
std::terminate() - Гарантия — компилятор берет на себя ответственность
- Оптимизация — контейнеры и move семантика полагаются на этот контракт
Никогда не выбрасывай исключения из деструкторов.