cpp-ru/ideas

Добавление warning при вызове функции, кидающей исключение, из noexcept функции

cezarnik opened this issue · 10 comments

Предложение - чтобы при указании какого-то флага компилятор (-Wnoexcept-safe, помощь в названии флага приветствуется) мог указывать, что в такой-то noexcept функции может быть вызвана функция, кидающая исключение.

Часто, когда пишешь код без исключений (например, когда их выкидывание очень сильно влияет на производительность), хочется передавать ошибку как возвращаемый результат (например, через std::expected), а саму функцию помечать как noexcept.
Сейчас выкидывание исключения из noexcept функции приводит к вызову std::terminate. Это делает спецификатор noexcept очень непрактичным для каких-то нетривиальных функций:
Сделав функцию A noexcept, автор должен быть уверен, что никакие функции, которые он позовёт из A, не должны бросать исключение. Что ещё более страшно, что функции, которые вызываются из A, могут менять свою спецификацию или начать кидать исключения, что ставит A под угрозу вызова std::terminate
Более того, все деструкторы обычно noexcept, и изменение любой функции, вызываемой в деструкторе, может вызвать поломку этого кода, поэтому сейчас используются конструкции такого вида

~MyClass(){
  try {
    CallSomeFunctions();
  } catch(...) {
   // Do nothing.
  }
}

Можно оставить реализацию с std::terminate при таком вызове для обратной совместимости, но для всех желающих компиляция с новым флагом -Werror позволит избежать таких проблем.

В реализации могут быть трудности с определением, кинется ли исключение в случае, если у нас есть try/catch блок в нашей noexcept функции, который ловит что-то кроме ... - может быть такой случай:

class  MyException : public std::exception{};
class  AnotherException : public std::exception{};

CallSomeFunctions() {
   throw AnotherException();
}

~MyClass(){
  try {
    CallSomeFunctions();
  } catch(const MyException& myEx) {
   // Do nothing.
  }
}

В данном случае вылетит исключение. Чтобы такое понимать, надо для простоты либо считать, что exception может вылететь всегда, если не ловится ..., либо поддерживать все виды выбрасывемых типов, и проверять, что они являются наследниками типа, который написан в catch. Второй вариант, мне кажется, не сойдётся, так как может быть всякая экзотика с вызовом std::function, которая, если не пометить её как noexcept (так можно?), может кидать произвольные исключения

В случае, если в noexcept функции нет try/catch блоков, то проверка корректности заключается в проверке спецификатора noexcept у всех вызываемых функций. Пример ниже должен кинуть warning (даже если в B ничего не бросается)

A() noexcept {
 B();
}

B() {
}

Предлагаемый анализ будет выдавать false positive в таком случае:

void f(optional<int> o) noexcept {
  if (o)
    o.value();
}

В данном случае из noexcept функции f безопасно звать optional::value() потенциально бросающую исключение bad_optional_access, так как этот вызов происходит после соответствующей проверки o.has_value(), но анализатор все равно будет выдавать предупреждение.

void f(optional<int> o) noexcept {
  if (o)
    o.value();
}

Подобный код всегда не оптимален

Это не важно, оптимален ли он, так пишут, а значит warning добавлять нельзя.

Всё так, спасибо за замечание.
Но никто не предлагает ломать существующий код - предлагается лишь добавить опцию компилятора как opt-in. Конкретно с такой реализацией автор получит warning, только если включит флаг. А люди, которые захотят получать помощь от компилятора, смогут поправить код (в примере выше всё очень легко чинится).

Как предлагается быть с таким кодом?

void f(std::vector<int> v) {
  if (!v.empty()) {
    v[0];
  }
}

Он абсолютно корректен и не может бросить исключение. При этом std::vector::operator[] не помечен noexcept. Исходя из описанной логики анализа, здесь тоже будет ложноположительное срабатывание, которое можно исправить лишь ненужным try-catch блоком.

Да, с этим примером беда. Но, честно говоря, я не понимаю, почему он не noexcept, ведь при выходе за пределы может быть что угодно. Я хочу сказать, что это камень как будто не в огород предложения. Если есть причина, по которой не стоит помечать оператор как noexcept, поправь меня, пожалуйста.
Меня, правда, начало смущать, что у вектора есть методы, которые бросают bad_alloc, что сразу запрещает их использование в noexcept функциях, но, может быть, это и не плохо.

Но да, предложение подразумевает следующее: чтобы включить эту опцию в компиляторе, нужно переписать код, и на ранних стадиях можно будет писать немного функций такого вида. Но есть надежды, что по мере использования множество noexcept функций расширится

Если есть причина, по которой не стоит помечать оператор как noexcept, поправь меня, пожалуйста.

Причина есть - реализации хотят иметь возможность вставлять в отладочном режиме дополнительные проверки и в том числе бросать исключения. Так делает стандартная библиотека MSVC, например. Именно поэтому многие функции, которые могут привести к UB, не помечены noexcept.

void f(optional<int> o) noexcept {
  if (o)
    o.value();
}

Подобный код всегда не оптимален

С нормальным уровнем оптимизации компиляторы выкинут код со второй проверкой на пустоту и бросанием исключения.

clang-tidy ловит по крайней мере часть таких проблем -> https://clang.llvm.org/extra/clang-tidy/checks/bugprone/exception-escape.html