5.5 KiB
Ложный noexcept
Начиная с 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;
}
}
Может быть очень сложно понять почему это произошло, если код разнесен по разным единицам трансляции.
Условный noexcept
В С++ любят экономить на ключевых словах.
= 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");
}
};