mirror of
https://github.com/Nekrolm/ubbook.git
synced 2025-12-18 13:14:41 +03:00
159 lines
7.0 KiB
Markdown
159 lines
7.0 KiB
Markdown
# Function-try-block
|
||
|
||
В C++ существует альтернативный синтаксис для определения тела функции, позволяющий навесить на него целиком перехват и обработку исключений
|
||
|
||
```C++
|
||
// Стандартный способ
|
||
void f() {
|
||
try {
|
||
may_throw();
|
||
} catch (...) {
|
||
handle_error();
|
||
}
|
||
}
|
||
|
||
// Альтернативный синтаксис
|
||
void f() try {
|
||
may_throw();
|
||
} catch (...) {
|
||
handle_error();
|
||
}
|
||
```
|
||
|
||
Во-первых, запись становится короче, с меньшим уровнем вложенности.
|
||
Во-вторых, эта фича позволяет нам ловить исключения там, где стандартным способом это сделать невозможно — в списке инициализации класса, при инициализации подобъекта базового класса и подобном.
|
||
|
||
```C++
|
||
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;
|
||
};
|
||
```
|
||
|
||
На [примере](https://godbolt.org/z/6Yf5cE7W4) с `try-block` для конструктора мы сталкиваемся с, на первый взгляд странной, неожиданностью: несмотря на блок `catch`, исключение вылетает в код, вызывающий конструктор.
|
||
Это логично, ведь если при инициализации полей класса вылетело исключение, мы никак не можем исправить ситуацию и починить объект.
|
||
|
||
Потому можно иногда встретить такие страшные нагромождения
|
||
```C++
|
||
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`, который бы гарантированно перехватил все, весьма недурна.
|
||
|
||
```C++
|
||
struct DctorThrowTry {
|
||
~DctorThrowTry() try {
|
||
throw std::runtime_error("err");
|
||
} catch (const std::exception& e) {
|
||
std::cout << e.what() << "\n";
|
||
}
|
||
};
|
||
```
|
||
|
||
Выглядит неплохо. Но у нас C++, так что это [не работает](https://godbolt.org/z/vMhb8nWvq)!
|
||
|
||
Кто-то очень доброжелательный решил, что в случае с деструкторами поведение по умолчанию должно быть таким же как и с конструкторами. То есть **`catch` блок деструктора неявно прокидывает исключение дальше**. И привет всем возможным проблемам с исключениями из деструкторов, в том числе нарушению неявного `noexcept(true)`.
|
||
|
||
Однако, в отличие от конструкторов, для деструкторов добавили возможность подавить неявное пробрасывание пойманного исключения. Для этого нужно всего лишь... добавить `return`!
|
||
|
||
```C++
|
||
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` блок они все уже мертвы.
|
||
|
||
```C++
|
||
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;
|
||
}
|
||
```
|
||
|
||
**Итого**
|
||
|
||
1. Для обычных функций и `main()` с помощью альтернативного синтаксиса можно удобно и красиво перехватывать все исключения, которые могли бы вылететь. И поведение по умолчанию — именно перехват. [Дальше не летит.](https://godbolt.org/z/eYGevjeco)
|
||
2. Для конструкторов можно ловить исключения из конструкторов полей, обрабатывать их (печатать в лог), но подавить нельзя. Либо кидаете свое новое исключение, либо пойманное неявно будет проброшено дальше.
|
||
3. Для деструкторов также будет неявный проброс, но его можно подавить, добавив `return`.
|
||
|
||
Если что, в Rust нет исключений (но есть очень похожие паники). Живите с этим.
|
||
|
||
### Полезные ссылки
|
||
1. https://en.cppreference.com/w/cpp/language/function-try-block
|
||
2. https://mariusbancila.ro/blog/2019/03/13/little-known-cpp-function-try-block/ |