Исключения
Исключительная ситуация - следствие вызова операции с ошибкой, которая не может или не должна быть проигнорирована.
Например, деление на ноль вне типа double
. К чему это должно привести? Непонятно, поэтому кидаем исключение.
Другой пример: dynamic_cast
не смог привести тип к другой ссылке. Что нужно вернуть, чтобы принимающий знал, что операция завершилась неудачей? Нулевой ссылки не бывает, значит, кидаем исключение.
Для генерации исключения в C++ используется слово throw
. Бросать можно практически все, что угодно (можно сказать, все, что можно сконструировать). Где будет выделена память под объект, который будем бросать - не специфицировано.
Для отлавливания исключений используем блоки try
и catch
.
struct A {};
struct B : A {};
try {
// бросает B
f();
} catch (B b) {
// так можно
} catch (const B& b) {
// так можно
} catch (B&& b) {
// ошибка компиляции
} catch (const A&) {
// так можно
} catch (A a) {
// так можно, но осторожно
} catch (...) {
// так можно, ловит все
}
Поиск обработчика при появлении исключения в блоке try
идет последовательно, в объявленном разработчиком порядке, поэтому рекомендуется писать обработчики от частного к общему.
Если обработчик не был найден, исключение покинет блок try
, и стек продолжит разворачиваться.
Конструкцию throw
можно писать по-разному:
struct A {};
struct B : A {};
void f() {
throw B(); // B
throw new B(); // B*
}
try {
// бросает B
f();
} catch (B b) {
throw b;
// new B1 will be copied on catch(B b)
// new B2 will be copied on throw
// B2 will be thrown
// B and B1 will be destroyed
} catch (B b) {
throw;
// new B1 will be copied on catch(B b)
// B1 will be thrown
// B will be destroyed
} catch (const B& b) {
throw b;
// new B1 will be copied on throw
// B1 will be thrown
// B will be destroyed
} catch (const B& b) {
throw;
// B will be thrown
} catch (const A&) {
throw;
// B will be thrown
} catch (A a) {
throw;
// B will be thrown (check on gotbolt, i'm not sure)
} catch (...) {
throw;
// caught object will be thrown
}
noexcept
При использовании после объявления функции ключевое слово noexcept
разворачивается в конструкцию noexcept(true)
, означающую "эта функция никогда не бросает исключения".
Гарантия с нашей стороны позволяет компилятору генерировать более компактный код. Однако, если мы нарушим гарантию, и исключение будет брошено из noexcept
функции, то мгновенно произойдет вызов std::terminate
и по умолчанию программа аварийно завершится без запуска каких-либо деструкторов.
void f() noexcept {}
void g() noexcept {
throw "oops";
} // calls std::terminate()
В конструкцию noexcept(expr)
после объявления функции можно писать любое статическое булево выражение. Таким образом гарантия исключений шаблонных функций может, например, зависеть от переданного в них типа:
template <typename T>
void foo(T&& t) noexcept(std::is_trivially_destructible_v<T>);
В другом случае noexcept(expr)
служит булевым оператором, проверяющим, является ли переданная в unevaluated
-контексте функция noexcept
-функцией.
struct A {
~A() {
// non-trivial
}
};
static_assert(noexcept(foo<A>(std::declval<A>())) == 0);
Часто используется в таком виде:
template <typename T>
void bar(T&& t) noexcept(noexcept(f<T>(std::declval<T>())));
Исключения в деструкторах
Деструкторы, начиная со стандарта C++11, неявно помечены как noexcept
, то есть им не разрешается бросать исключения вообще, иначе произойдет мгновенный вызов std::terminate
.