Files
ubbook/syntax/function-try-catch.md
2022-02-20 21:54:26 +02:00

159 lines
7.0 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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/