В C++ существует альтернативный синтаксис для определения тела функции, позволяющий навесить на него целиком перехват и обработку исключений
// Стандартный способ
void f() {
try {
may_throw();
} catch (...) {
handle_error();
}
}
// Альтернативный синтаксис
void f() try {
may_throw();
} catch (...) {
handle_error();
}
Во-первых, запись становится короче, с меньшим уровнем вложенности. Во-вторых, эта фича позволяет нам ловить исключения там, где стандартным способом это сделать невозможно — в списке инициализации класса, при инициализации подобъекта базового класса и подобном.
struct ThrowInCtor {
ThrowInCtor() {
throw std::runtime_error("err1");
}
};
struct TryStruct1 {
TryStruct1() try {
} catch (const std::exception& e) {
// будет поймано исключение из конструктора `c`
std::cout << e.what() << "\n";
}
ThrowInCtor c;
};
struct TryStruct2 {
TryStruct2() {
try {
} catch (const std::exception& e) {
// исключение не будет поймано, поскольку тело конструктора
// исполняется после инициализации полей
std::cout << e.what() << "\n";
}
}
ThrowInCtor c;
};
На примере с try-block
для конструктора мы сталкиваемся с, на первый взгляд странной, неожиданностью: несмотря на блок catch
, исключение вылетает в код, вызывающий конструктор.
Это логично, ведь если при инициализации полей класса вылетело исключение, мы никак не можем исправить ситуацию и починить объект.
Потому можно иногда встретить такие страшные нагромождения
struct S {
S(...) try :
a(...),
b(...) {
try {
init();
} catch (const std::exception& e) {
log(e);
try_repair();
}
} catch (const std::exeption& e) {
// не получилось починить или неисправимая ошибка в полях
log(e);
// implicit rethrow
}
A a;
B b;
};
Ну хорошо. А как насчет деструкторов? Ведь из деструкторов крайне нежелательно выкидывать исключения, и возможность красиво и просто поставить catch
, который бы гарантированно перехватил все, весьма недурна.
struct DctorThrowTry {
~DctorThrowTry() try {
throw std::runtime_error("err");
} catch (const std::exception& e) {
std::cout << e.what() << "\n";
}
};
Выглядит неплохо. Но у нас C++, так что это не работает!
Кто-то очень доброжелательный решил, что в случае с деструкторами поведение по умолчанию должно быть таким же как и с конструкторами. То есть catch
блок деструктора неявно прокидывает исключение дальше. И привет всем возможным проблемам с исключениями из деструкторов, в том числе нарушению неявного noexcept(true)
.
Однако, в отличие от конструкторов, для деструкторов добавили возможность подавить неявное пробрасывание пойманного исключения. Для этого нужно всего лишь... добавить return
!
struct DctorThrowTry {
~DctorThrowTry() try {
throw std::runtime_error("err");
} catch (const std::exception& e) {
std::cout << e.what() << "\n";
return; // исключение не будет перевыброшено!
}
};
Удивительно, но таким образом в C++ есть случай, в котором return
последней командой в void-функции меняет ее поведение.
Также нужно добавить, что в catch блоке деструкторов и конструкторов нельзя обращаться к нестатическим полям и методам класса — будет неопределенное поведение. По понятным причинам. В момент входа в catch
блок они все уже мертвы.
struct S {
A a;
B b;
S() try {
...
} catch (...) {
do_something(a); // UB!
}
~S() try {
...
} catch (...) {
do_something(b); // UB!
return;
}
};
// Но при этом
bool fun(T1 a, T2 b) try {
...
return true;
} catch (...) {
// важно: этот блок не ловит исключения, возникающие при инициализации a и b
do_something(a); // Ok!
return false;
}
Итого
- Для обычных функций и
main()
с помощью альтернативного синтаксиса можно удобно и красиво перехватывать все исключения, которые могли бы вылететь. И поведение по умолчанию — именно перехват. Дальше не летит. - Для конструкторов можно ловить исключения из конструкторов полей, обрабатывать их (печатать в лог), но подавить нельзя. Либо кидаете свое новое исключение, либо пойманное неявно будет проброшено дальше.
- Для деструкторов также будет неявный проброс, но его можно подавить, добавив
return
.
Если что, в Rust нет исключений (но есть очень похожие паники). Живите с этим.