На написание этой заметки подтолкнула статья "Топ-10 ошибок в C++ проектах за 2022 год", а если точнее, то ошибка, которая там поставлена на шестое место. В двух словах: в операторе перемещения некого класса вызывается метод setName, в котором может быть брошено исключение. При этом сам оператор перемещения помечен как noexcept.
На мой взгляд, ситуация здесь не так проста, как может показаться.
И дело в том, что некоторые C++ программисты (включая меня самого в прошлом) смотрят на маркер noexcept (не бросает) как на маркер nofail (не сбоит, всегда OK). Т.е., если у нас есть noexcept-функция, то она не может завершиться с ошибкой.
В каких-то случаях так оно и есть. Например, если мы делаем swap для двух int-ов или для двух обычных указателей, то странно ожидать появление ошибки в таком swap.
Однако, noexcept означает nofail далеко не всегда.
Зато noexcept всегда означает norecover (если сбоит то без шансов на продолжение). Т.е., если в noexcept функции что-то пошло не так, то восстановиться до какого-то вменяемого состояния у нас уже нет возможности.
К сожалению, в C++ nofail от norecover не отличимы. Мы можем написать только noexcept, но не можем дать дополнительной информации: гарантируем ли мы отсутствие ошибок или же лишь подтверждаем, что при возникновении ошибки восстановление невозможно и единственный разумный путь -- это std::terminate.
И если рассматривать noexcept именно как norecover, то вызовы бросающих исключение функций в noexcept-функциях становятся вполне себе оправданными. Какой смысл оборачивать бросающие функции в try..catch чтобы в catch самому вызвать std::abort? Сам по себе noexcept сделает это не хуже нас.
В общем, я постепенно пришел к тому, чтобы рассматривать noexcept-функции прежде всего как norecover-функции. И только в некоторых, чаще всего в тривиальных случаях, как nofail-функции.
А посему теперь спокойно отношусь и к тому, что в noexcept-функция могут запросто вызываться бросающие исключения функции без оборачивания их в try..catch. И не считаю такое поведение ошибкой.
Но здесь вновь нужно сказать, что лично мне очень сильно не хватает в C++ noexcept-блока. Пишешь такой блок, а компилятор выдает предупреждение, если внутри noexcept-блока вызываются не-noexcept-функции.
Полезно такое иметь потому, что:
- человеку свойственно ошибаться и я могу думать, что вызванная мной функция f() является noexcept, а на самом деле нет;
- все течет, все изменяется. Изначально f() могла быть noexcept, но со временем ее могли модифицировать и убрать маркер noexcept. Но я, как пользователь f(), об этом даже не узнаю.
Не думаю, однако, что когда-нибудь в C++ noexcept-блок появится.
Да и вообще, глядя на то, что подобавляли в C++20 (например, модули, в которые без поллитры не въедешь) и в C++23 (например, deducing this), все больше и больше прихожу к заключению, что я недостаточно умен, чтобы освоить настолько сложный инструмент :(
PS. Возвращаясь к упомянутой выше статье. Думаю, что предложенные автором статьи способы исправления обнаруженной анализатором "ошибки", мягко говоря, неуместны. Там нужно было копать совсем в другую сторону (а то и в несколько разных сторон).