cpp-ru / ideas

Идеи по улучшению языка C++ для обсуждения
https://cpp-ru.github.io/proposals
Creative Commons Zero v1.0 Universal
89 stars 0 forks source link

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

Open cezarnik opened 1 year ago

cezarnik commented 1 year ago

Предложение - чтобы при указании какого-то флага компилятор (-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() {
}
AndreyG commented 1 year ago

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

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

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

vtopunov commented 1 year ago
void f(optional<int> o) noexcept {
  if (o)
    o.value();
}

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

AndreyG commented 1 year ago

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

cezarnik commented 1 year ago

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

Smertig commented 1 year ago

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

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

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

cezarnik commented 1 year ago

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

cezarnik commented 1 year ago

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

Smertig commented 1 year ago

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

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

adromanov commented 1 year ago
void f(optional<int> o) noexcept {
  if (o)
    o.value();
}

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

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

asherikov commented 1 year ago

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