Files
2022-03-20 12:49:46 +03:00

109 lines
5.5 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.
# Ложный `noexcept`
Начиная с 11 стандарта, мы можем помечать функции и методы спецификатором `noexcept`, говоря тем самым компилятору, что эта функция или метод не бросает исключения.
И вроде бы все хорошо: получив такую информацию, компилятор может не генерировать дополнительные инструкции для обработки раскрутки стека. Бинарники становятся меньше, а программы быстрее.
Но проблема в том, что этот спецификатор не заставляет компиляторы проверять,
что функция действительно не бросает исключений.
Если мы пометим функцию как `noexcept`, а она возьмет да кинет исключение,
произойдет что-то странное, заканчивающееся внезапным `std::terminate`.
Так, например, неожиданно [перестанут работать](https://godbolt.org/z/E9c9Ya) `try-catch` блоки.
```C++
void may_throw(){
throw std::runtime_error("wrong noexcept");
}
struct WrongNoexcept {
WrongNoexcept() noexcept {
may_throw();
}
};
// Попытки обернуть в try-catch эту функцию или любой код,
// использующий ее — бесполезны.
void throw_smth() {
if (rand() % 2 == 0) {
throw std::runtime_error("throw");
} else {
WrongNoexcept w;
}
}
```
Может быть очень сложно понять почему это произошло, если код разнесен по разным единицам трансляции.
## Условный `noexcept`
В С++ любят экономить на ключевых словах.
- `= 0` для объявления чисто виртуальных методов
- новый `requires` имеет два значения, порождая странные конструкции `requires(requires(...))`
- `auto` и для автовывода, и для переключения на trailing return type
- `decltype`, у которого разный смысл при применении к переменной и к выражению
- и, конечно, `noexcept` — точно также два значения как у `requires`.
Есть спецификатор `noexcept(condition)`. И просто `noexcept` — синтаксический сахар
для конструкции `noexcept(true)`.
А есть предикат `noexcept(expr)`, проверяющий, что выражение `expr` не кидает исключений по самой своей природе (сложение чисел, например) или же
помечено как `noexcept`.
И вместе они порождают конструкцию для условного навешивания noexcept:
```C++
void fun() noexcept(noexcept(used_expr))
```
```C++
void may_throw(){
throw std::runtime_error("wrong noexcept");
}
struct ConditionalNoexcept {
ConditionalNoexcept() noexcept(noexcept(may_throw())) {
may_throw();
}
};
// теперь с этой функцией все хорошо
void throw_smth() {
if (rand() % 2 == 0) {
throw std::runtime_error("throw");
} else {
ConditionalNoexcept w;
}
}
```
Чтобы избежать проблем, нужно всегда и везде использовать условный `noexcept` с аккуратной проверкой каждой используемой функции, либо вовсе не использовать `noexcept`. Но во втором случае стоит помнить,
что операции перемещения, а также `swap`, должны помечаться как `noexcept` (и быть действительно `noexcept`!) для эффективной работы со стандартными контейнерами.
Не забывайте писать негативные тесты. Без них
можно проморгать появление ложного `noexcept` и получить `std::terminate` на боевом стенде.
Также обратите внимание на тонкий и неприятный нюанс: если вам ну очень сильно надо кидать исключения из деструктора, обязательно явно пишите в его объявлении `noexcept(false)`. По умолчанию все ваши функции и методы помечены неявно `noexcept(false)`, но для деструкторов в C++ сделано исключение. Они неявно помечены `noexcept(true)`. [Так что](https://godbolt.org/z/5jo95d):
```C++
struct SoBad {
// invoke std::terminate
~SoBad() {
throw std::runtime_error("so bad dctor");
}
};
struct NotSoBad {
// OK
~NotSoBad() noexcept(false) {
throw std::runtime_error("not so bad dctor");
}
};
```
## Полезные ссылки
1. https://en.cppreference.com/w/cpp/language/noexcept
2. https://en.cppreference.com/w/cpp/language/noexcept_spec
3. https://www.modernescpp.com/index.php/c-core-guidelines-the-noexcept-specifier-and-operator