Начиная с 11 стандарта, мы можем помечать функции и методы спецификатором noexcept
, говоря тем самым компилятору, что эта функция или метод не бросает исключения.
И вроде бы все хорошо: получив такую информацию, компилятор может не генерировать дополнительные инструкции для обработки раскрутки стека. Бинарники становятся меньше, а программы быстрее.
Но проблема в том, что этот спецификатор не заставляет компиляторы проверять, что функция действительно не бросает исключений.
Если мы пометим функцию как noexcept
, а она возьмет да кинет исключение,
произойдет что-то странное, заканчивающееся внезапным std::terminate
.
Так, например, неожиданно перестанут работать try-catch
блоки.
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;
}
}
Может быть очень сложно понять почему это произошло, если код разнесен по разным единицам трансляции.
В С++ любят экономить на ключевых словах.
= 0
для объявления чисто виртуальных методов- новый
requires
имеет два значения, порождая странные конструкцииrequires(requires(...))
auto
и для автовывода, и для переключения на trailing return typedecltype
, у которого разный смысл при применении к переменной и к выражению- и, конечно,
noexcept
— точно также два значения как уrequires
.
Есть спецификатор noexcept(condition)
. И просто noexcept
— синтаксический сахар
для конструкции noexcept(true)
.
А есть предикат noexcept(expr)
, проверяющий, что выражение expr
не кидает исключений по самой своей природе (сложение чисел, например) или же
помечено как noexcept
.
И вместе они порождают конструкцию для условного навешивания noexcept:
void fun() noexcept(noexcept(used_expr))
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)
. Так что:
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");
}
};