Комментарии 67
А именно — что нельзя звать
dlopen
вот в этот момент, мы уже всё закешировали.
то есть теперь запрещено открывать динамические библиотеки после старта программы? А в scilla db проверка эта добавлена или там просто запрещено?
Запрещено открывать динамические библиотеки после инициализации всех компонентов. То есть, в конструкторах можно спокойно использовать dlopen|dlclose|dlsym, но когда уже сервер полностью стартовал и начал обрабатывать пользовательские запросы - нельзя.
В исходниках scilla найти переопределение dlopen|dlclose|dlsym не получилось, так что наверное у них просто запрещено в документации (или запрещено как-то более хитро).
Добавлять что-то к nullptr - это UB.
Только если до этой операции дойдёт выполнение.
Согласен, действительно грязный трюк :)
Но ведь UB -- это не просто неизвестный результат гипотетического выполнения (и если выполнения не будет, то вроде как и не важно какой), это ещё и контракт с компилятором.
Компилятор исходит из предположения, что UB в программе нет, и делать выводы на основе этого предположения он будет безотносительно того, а дойдет ли докуда выполнение на самом деле.
То есть компилятор например может перекособочить выражение где используется этот "declval на коленке", рассудив что вся конструкция в таком виде невозможна раз это выглядит как UB. И что нужно проигнорировать данную попытку SFINAE, выдать другое значение, взять другой тип и так далее в том же духе.
Примеров творческой интерпретации программы компилятором из-за наличия в ней UB полно и кажется наивным полагать, что это ни в коем случае не касается compile-time выражений.
Это буквально чушь. Порядок выбора перегрузки, их ordering, и так далее - это зафиксировано в стандарте.
Вы в курсе, что все гарантии стандарта распространяются только на программу без UB?
UB - это runtime concept, а не compile-time. Пока код с UB не исполняется в реальности, UB не существует. Например, здесь нет UB:
if (false) {
int *i = nullptr;
*i += 1;
}
Здесь UB может быть, а может и не быть:
int incr(int x)
{
return ++x;
}
Когда пишется код типа такого:
T x;
decltype(do_something(x)) y;
никакого UB быть не может, поскольку никакого кода, который мог бы выполняться при вызове do_something()
, вообще не генерируется. Выполняется только type inference, а его правила четко определены в стандарте и никак не зависят от конкретных входных данных.
никакого UB быть не может, поскольку никакого кода, который мог бы выполняться при вызове
do_something()
, вообще не генерируется
template<typename T> auto do_something(T&&) {
/* какое-нибудь UB */
}
Гипотетически компилятор натыкается на нестыковку, оптимизирует do_something()
до return nullptr;
и вуаля, decltype()
возвращает вообще не то. Хотя никакого кода не генерируется и более того, в стандарте прямым текстом написано, что передаваемое выражение НЕ выполняется.
Пример конечно очень частный, но иллюстрирует что лучше никогда не говорить "никогда" =) особенно если поведение программы буквально не определено.
Пока код с UB не исполняется в реальности, UB не существует.
Но возможное наличие UB сказывается на генерируемой программе еще во время ее компиляции.
UB - это runtime concept, а не compile-time.
Потому что нет, это не только runtime concept.
Помимо названия для последствий некоторых действий, это еще и негласное обещание компилятору, что программа таких действий производить не будет.
На основе этого обещания компилятор делает самые разные выводы как раз-таки в compile time, в основном в целях оптимизации. Думаю многие видели как потенциальное UB приводит к изменению фактического алгоритма вплоть до неузнаваемости еще задолго до его выполнения.
Гипотетически компилятор натыкается на нестыковку, оптимизирует
do_something()
доreturn nullptr;
и вуаля,decltype()
возвращает вообще не то. Хотя никакого кода не генерируется и более того, в стандарте прямым текстом написано, что передаваемое выражение НЕ выполняется.
Это так не работает. Вывод типа выполняется до генерации какого-либо кода, и уж тем более до выполнения каких-либо оптимизаций, и в этом отношении все там полностью определено.
Помимо названия для последствий некоторых действий, это еще и негласное обещание компилятору, что программа таких действий производить не будет.
Да, в рантайме. И она их не производит (практически как в моем примере с if (false) {...}
). Слово "поведение" в словосочетании "неопределенное поведение" относится к поведению виртуальной машины C++, которую описывает стандарт, при выполнении сгенерированного компилятором кода. Нет выполняемого кода - нет и виртуальной машины, которая может "неопределенно себя повести". Не нужно распространять понятие UB на компилятор, там у него есть свои заморочки типа IFNDR, но это уже совсем другая история.
Это так не работает. Вывод типа выполняется до генерации какого-либо кода
Это как это вывод типа auto без trailing return type работает до генерации кода return?
Да, в рантайме. И она их не производит
Поэтому программный код, который расходится с этим утверждением, компилируется не всегда предсказуемым образом. Я бы даже сказал часто очень неожиданным образом компилируется =).
Слово "поведение" в словосочетании "неопределенное поведение" относится к поведению виртуальной машины C++ <...> Не нужно распространять понятие UB на компилятор
Да, фундаментально UB это наблюдаемое неопределенное поведение как результат буквального выполнения ошибочных инструкций.
Но компилятор не просто транслирует код один к одному, он еще пытается выполнять всяческие оптимизации, в том числе исходя из предположения что никакого UB в природе нет и быть не может.
В ряде случаев попытка скомпилировать код, в котором есть UB, будучи уверенным что ничего там нет, вместо корректной оптимизации приводит к некорректной и непредсказуемой модификации исходной программы.
Поэтому изначальное утверждение, которому я оппонировал:
Добавлять что-то к nullptr - это UB, только если до этой операции дойдёт выполнение
Не выглядит справедливым. Наличие UB в коде может привести и нередко приводит к непредсказуемым последствиям в плане генерируемого кода, делая поведение не определенным стандартом даже если фактически выполнение до этой конкретной операции еще не дошло.
Это как это вывод типа auto без trailing return type работает до генерации кода return?
Генерация исполняемого кода для этого не нужна. Выполнение каких-либо оптимизаций тем более не нужно. А уж в случае если возвращаемый тип известен из объявления (даже если он шаблонный, как в примере из этой статьи), то внутри функции может быть вообще практически что угодно.
Вообще с разыменованием нулевого указателя интересно. Я, конечно, не настоящий сварщик, но максимум, что нашел в стандарте, так это такую формулировку, кажущуюся мне странной
In particular, a null reference cannot exist in a well-def-ined program, because the only way to create such a
reference would be to bind it to the “object” obtained by indirection through a null pointer, which causes undefined behavior.
С другой стороны, есть std::bad_typeid, который получается как раз во время разыменовывания nullptr. Полагаю, что UB все же возможно в случаях, если разыменовывающее выражение не является тем, что стандарт называет unevaluated operand (клаусула expr.context). А еще шаблон помечен как constexpr, а в compile-time UB быть не может. Кмк, тут все же его нету. Поправьте меня, если ошибаюсь.
А для чего вообще нужно тело в замене std::declval?
Разве так не достаточно?
template<typename T>
constexpr T my_declval();
struct NonDefault
{
NonDefault() = delete;
int foo() const { return 1; }
};
int main()
{
decltype(my_declval<NonDefault>().foo()) n = 42;
return n;
}
"Compile-time-трюки" вообще не компилится (g++ 14.2).
Требуется C++20 стандарт с поддержкой auto в шаблонных параметрах
Ничего из: -std=c++20 -std=c++23 -std=c++26 не помогает
всё равно ругается на вызовы print:
print<&s.the_member_name>();
и
print<&a>();
print<&b>();
Вот так работает https://godbolt.org/z/b3TMxnj39
А почему бы не воспользоваться consteval для функции unsafe_do_something, чтобы запретить вызов в рантайме?
Полученный механизм весьма удобен, если вы, скажем, пользуетесь Continuous Integration. Когда какой‑то тест проваливается, из него вылетает исключение, попробуй разберись, откуда оно вылетело и почему так произошло.
Но ведь проще в CI
тесты под GDB
запустить, а ему передать команду печатать stacktrace
в случае падения. Никаких грязных трюков.
Когда же в плюсах наконец откажутся от исключений...
А зачем плюсам от них отказываться? Не нравятся - не используйте. Гугл и Qt как то же живут.
Я бы с удовольствием их не использовал (я и не использую их в СВОЁМ коде), если бы это было возможно.
Ваш ответ - глупый, по той простой причине, что исключения в плюсах - отличный инструмент для стрельбы по ногам, при этом даже по задумке он выглядит рудиментом со времен появления шаблонов (ради справедливости, в других языках исключения выглядят лишь чуть лучше за счёт большего порядка). Пока такие инструменты есть, есть и повод кричать о небезопасности плюсов и как следствие, принуждать отказываться от них: https://habr.com/ru/articles/856804/
Что самое смешное, в плюсах уже всё есть, чтобы отказаться от них. Есть optional, который можно расширить до "result". А сами исключения - всего лишь преобразовать в панику. Я так понимаю, лет через 10 опомнятся и наконец завезут )
я и не использую их в СВОЁМ коде
А ваш код где-нибудь в открытом доступе есть? Очень хочется посмотреть как люди пишут надежный код только на кодах возврата, совсем без исключений.
Вы это серьёзно? Для этого не нужно смотреть именно мой код. Посмотрите любой код на всяких си, руст, голанг ). Если мало, посмотрите на потоки STL и ту кривую часть filesystem, где коды возврата сделаны не так, как нужно.
Не очень понятно, как кривые плюсовые исключения в вашем сознании положительно влияют на надёжность. Потому что всё совсем наоборот.
Вы это серьёзно?
Абсолютно.
Для этого не нужно смотреть именно мой код.
Может быть, но мне интересно посмотреть на то, как пишут код настолько упоротые противники исключений.
Посмотрите любой код на всяких си
Время от времени приходится. Там либо забивают на часть возможных проблем (например, часто не контролируются результаты strdup или sprintf). Либо же сразу вызывают abort
и все.
голанг
Там сплошное if err != nil
из-за которых прикладная логика теряется под толщей подобных if-ов и, плюс к тому, в Go есть паники.
Не очень понятно, как кривые плюсовые исключения в вашем сознании положительно влияют на надёжность.
То, что вам не понятно -- это очевидно. Но вопрос не в этом, а в том, как писать надежный код без исключений на C++ (не Rust, не Go, а именно на C++). И примеров, как я понимаю, от вас не будет.
Насколько я понимаю — вот есть один вариант — https://ned14.github.io/outcome, насколько я понимаю пока нативной поддержки в языке не будет — там все держится на программисте и обмазано макросами. Затащить это в проект я не решился, хотя пробовал LLFIO: Main Page, в котором оно используется.
Кроме outcome есть еще и Boost.Leaf.
Может быть, но мне интересно посмотреть на то, как пишут код настолько упоротые противники исключений.
Упоротые - это ваши мысли про то, что кто-то якобы упорот. Упоротый сторонник исключений может глянуть код Qt. Много кода, много-много кода. В дополнение к тому, что указано раньше
Время от времени приходится. Там либо забивают на часть возможных проблем (например, часто не контролируются результаты strdup или sprintf). Либо же сразу вызывают
abort
и все.
Да-да, там забивают на часть. А в ваших исключишках забивают абсолютно все. И я даже не лезу в дебри, когда вообще невозможно угадать, какое исключение прилетит, и о чём оно вообще. В плюсах уже убрали декларации throw как абсолютно идиотское решение. Уберут и noexcept. Потом наверное и до самих исключишек дойдёт. Лет через 10, как всегда. Тут несколько обидно, что шаблоны появились 100 лет назад, а элементарно приложить их правильно догадались почему-то в других языках.
Там сплошное
if err != nil
из-за которых прикладная логика теряется под толщей подобных if-ов и, плюс к тому, в Go есть паники.
Хоспади... "прикладная логика" ))). "Прикладная логика" теряется из-за любителей выпендриваться в коде, любителей огромных вложенных циклов, а также из-за ленивых любителей исключений, которым лень написать всё как надо, без goto и без перекладывания своих проблем на того, кто будет пользоваться. А вот от линейной проверки и выхода из функции ничего не страдает.
В го есть паники, в русте есть паники, в си есть аборт (не будем тут углубляться). И это как раз тот случай, когда слышал звон... Все эти штуки 1) продуманы, в отличие от исключений плюсов. 2) предназначены для аварийного завершения программы в случае так называемых НЕИСПРАВИМЫХ ошибок. Да, есть defer (как и goto там есть), но идея в том, что паника не должна использоваться как обычный механизм обработки ошибок.
То, что вам не понятно -- это очевидно. Но вопрос не в этом, а в том, как писать надежный код без исключений на C++ (не Rust, не Go, а именно на C++).
Вам уже ответили на ваш вопрос. Надёжный код прекрасно пишется без исключений. И тьма примеров тому. Вот как вы пишете надёжный код с исключениями, тут совсем не понятно, ведь 1) надёжность будет зависеть от того, кто будет использовать ваш код. 2) если вы не робот, вы обязательно упустите исключение в какой-то функции. 3) при использовании либ, собранных разными компиляторами вся ваша "надёжность" на ваших исключишках идёт строем в одно место.
И примеров, как я понимаю, от вас не будет.
Лично от меня не будет. Примеры приведены выше. Или я прям должен по требованию анонима сбросить код коммерческих либ?
Упоротый сторонник исключений может глянуть код Qt. Много кода, много-много кода.
Ага, отличный пример того, как десятилетиями прятать голову в песок под мантры "bad_alloc-а не бывает, после bad_alloc-а жизни нет".
А можно ли в Qt-шном коде безопасно вызывать std::vector::at
или std::optional::value
?
Пока с наглядными примерами надежного кода без исключений у вас не получается. Еще попробуете?
А в ваших исключишках забивают абсолютно все.
Зря вы прибегаете к аргументам "зато у вас негров линчуют", клоуном от этого вы быть не перестаете, скорее наоборот.
предназначены для аварийного завершения программы в случае так называемых НЕИСПРАВИМЫХ ошибок. Да, есть defer (как и goto там есть), но идея в том
что если факты противоречат гипотезам, то тем хуже для фактов. В Go паники спокойно перехватываются, а как раз defer-ы позволяют чистить ресурсы для того, чтобы продолжить после восстановления без утечки чего-либо.
Надёжный код прекрасно пишется без исключений.
"прекрасно" -- это сильно (очень сильно) преувеличено. Но вот то, что это очень дорого и очень многословно -- это да.
И тьма примеров тому.
Пока от вас не увидел ни одного.
Вот как вы пишете надёжный код с исключениями, тут совсем не понятно
А мне как раз понятно почему вам непонятно.
надёжность будет зависеть от того, кто будет использовать ваш код
Надежность моего кода зависит от того, как я его написал. При этом я не несу ответственности за то, что кто-то как-то написал с использованием моего кода. Так что этот звидежь отправляем в /dev/null.
если вы не робот, вы обязательно упустите исключение в какой-то функции
А мне и не нужно ловить все исключения из каждой функции. Вот чего не понимаю упоротые противники исключений, так это того, что исключения ловятся гораздо реже, чем бросаются.
при использовании либ, собранных разными компиляторами вся ваша "надёжность" на ваших исключишках идёт строем в одно место.
Это какая-то страшилка из 1990-х. В современном мире, когда проекты собирается из open-source компонентов, такой проблемы не существует как класса. Более того, очень много C++ных проектов вообще затачивается по один единственный компилятор.
Или я прям должен по требованию анонима сбросить код коммерческих либ?
Ирония в том, что анонимом в данном разговоре являетесь вы.
Про имена.
А почему такое прямо в язык на завезут? Типа шарпового nameof()
который тоже работает в момент компиляции.
Парсинг __PRETTY_FUNCTION__ - дикий костыль, вызванный тем, что в стандарт языка всё ещё не завезли статичной рефлексии. Есть информация, когда её можно ожидать?
Во что компилятор превращает ключевое слово
throw
?
В весьма своеобразную форму goto
Что лучше — исключение или коды возврата?
Конечно коды возврата. Авторы Go и Rust могут только подтвердить (если С ники не в авторитете).
Коды возврата более менее реально покрыть и статическим анализатором (в части вот тут добавляем еще один код возврата в enum, айда теперь смотреть где у нас там выпали константы из switch (errcode) обработчиков), ну и автотестами с покрытием, в отличие от.
Но если проект унаследован или хочет C++ библиотек - то ничего не попишешь, придется сидеть с exceptions, вымирать как мамонт и далее.
Конечно коды возврата. Авторы Go и Rust могут только подтвердить
Python, C#, Java, JS и многие другие языки программирования с вами не согласятся. Да и в Rust паника - это исключение.
Статический анализ switch - хороший инструмент (есть нюансы с `default:`, и с большими кодовыми базами, с бинарными поставками). С исключениями же достаточно покрыть тестами catch блоки, которых будет значительно меньше чем проверок кодов возврата. И логика приложения не замусоривается лишними if err != nil
и последующими обработками ошибок.
Так что повторюсь: исключения и коды возврата - это два разных инструмента, для разных задач. Выбирайте то, что вам подходит больше для конкретной задачи
Python, C#, Java, JS и многие другие языки программирования с вами не согласятся. Да и в Rust паника - это исключение.
Паника есть паника, она не предусматривает обработки, только посмертный дамп.
А про Python, C#, Java, JS это вообще не аргумент, это все legacy из 90-х с заложенными еще тогда очень модными заблуждениями, а по сути - ошибками проектирования - мало кто понимал в те годы, как на самом деле нужно в надежность и прочие моменты.
С исключениями же достаточно покрыть тестами catch блоки, которых будет значительно меньше чем проверок кодов возврата.
Странный аргумент. Чего больше, чего меньше. Видимо пока еще не наступило понимание сути обработки ошибок в реальном коде.
Так что повторюсь: исключения и коды возврата - это два разных инструмента, для разных задач. Выбирайте то, что вам подходит больше для конкретной задачи
Не надо никуда повторяться. Если исключения продиктованы библиотеками - то от них не уйти, придется использовать, хочешь ты его или нет. Но как правило к обработке ошибок они имеют весьма... отдаленное отношение.
Разработчик библиотеки волен как чего угодно там внутри себе бросать в виде исключений, и никто ему не указ, да и никто их потом реально не проверит. Т.к. старые пользователи библиотеки и вовсе в неведении, что там в новой версии библиотеки им прилетит, сменили номер версии и ладушки, собралось - и в прод полетело.
В случае же с явными перечислениями тебя компилятор носом потыкает - что вот тут и тут и еще вот тут с новой версией библиотеки возможен новый код ошибки, а у тебя ее обработка не предусмотрена, вот и подумай теперь, что теперь тут может быть. В Java пытались с этим бороться, но потом в массе отчаялись - uncaughtException
наше все.
Паника есть паника, она не предусматривает обработки, только посмертный дамп.
Глупости. Вот пример того, как в коде на расте используют панику как прямой аналог исключений (в данном случае для отмены запросов).
А про Python, C#, Java, JS это вообще не аргумент, это все legacy из 90-х с заложенными еще тогда очень модными заблуждениями, а по сути - ошибками проектирования - мало кто понимал в те годы, как на самом деле нужно в надежность и прочие моменты.
Ну да, дураки были, не знали, что обработчики со switch (errcode)
из C образца семидесятых годов - это верх совершенства. Хорошо, что пришли вы, и развенчали их заблуждения.
Ну да, дураки были, не знали,
Ну почему дураки. Когда-то и на паровозах ездили по дорогам, и на телегах, не все же дураки поди были. Просто так тогда было модно или не могли иначе.
Вот и сейчас так делают по привычке, т.к. отказаться нельзя, терабайты legacy кода куда девать?
Но то что в не legacy некоторые даже и сейчас паровой двигатель пытаются прикручивать к телегам, как в примере выше про паники как "прямой аналог исключений", то это вообще странно конечно, как и то, что принуждение к явной проверке возможных ошибок через перечислимые типы от их возможных кодов вызывает прям столько негатива.
Стоило про контрактное программирование так долго рассказывать, чтоб потом это все хрясь об колено неверифицируемыми исключениями (как по сути нелокальными goto переходами), контракты эти.
Да и вообще коды ошибок - это да, явно нечто плохое, нужно срочно в шредер их все, от 404 до ORA-00060 и далее.
Ну не хотите писать статически верифицируемый и чуть более надежный код без goto (зачем-то называя их exceptions) - продолжайте и дальше использовать исключения, кто запрещает? Тем более в случае Modern C++ и выбора иного реально нет.
Хотя конечно забавно - goto/setjmp/longjmp это значит бубу, ни за что нельзя использовать! А exceptions - это найс, это можно, нужно и даже полезно. Хотя они в принципе, по сути - мало чем между собой отличаются (хоть и реализованы чуть иначе, но ведут-то себя абсолютно аналогично, к примеру при выходе из циклов for/while).
А почему так? Почему goto это плохо, а exceptions - это хорошо? В чем между ними разница? Чуть иной и более приятный глазу синтаксис, что еще? Одобрение и неодобрение старших товарищей и прочих авторов явно умных книг?
А почему при goto безусловные нелокальные переходы не одобряются, а при exceptions - наоборот одобряются? Только потому что другим синтаксисом оформлены? Самим это не кажется странным?
В случае же с явными перечислениями тебя компилятор носом потыкает - что вот тут и тут и еще вот тут с новой версией библиотеки возможен новый код ошибки, а у тебя ее обработка не предусмотрена
А теперь перейдём от компиляции к динамически подгружаемым библиотекам, и снова получаем: библиотека B обновилась, а библиотека A была не в курсе, что ей новый код ошибки обрабатывать.
Те же самые checked exceptions в Джаве были вашими «кодами возврата с обязательной проверкой». И так же программы не собирались, если не обработать новый тип исключения, который может быть выброшен из функции. Т.е. всё то, что вы ставите в плюс, в концепции исключений было. Но почему-то не прижилось.
Не надо путать коды возврата и обработку ошибок через типы-суммы. Для второй в языке, как минимум, должны быть эти самые типы-суммы. А чтобы она была ещё и удобной - требуется ещё несколько фич: проверка тотальности при работе с перечислениями (и типами-суммами), try-оператор, параметрический полиморфизм для обобщений по типу результата или по коду ошибки...
Для второй в языке, как минимум, должны быть эти самые типы-суммы.
Типы-суммы? Какой-то хаскель головного мозга, простите. Достаточно просто возвращать обычный составной тип, struct/class в терминах С++
С введением в практику destructuring assignment и type introspection это все довольно просто и удобно в итоге, к примеру в С++ уже сейчас вполне возможно писать нечто вида:
Проверяется через clang ./test.cpp --std=c++23 && ./a.out
#include <stdlib.h>
#include <stdio.h>
#define var auto
#define func auto
#define HANDLE_ERROR(val) fprintf(stderr, "Error in %s, wrong value: %d\n", __FUNCTION__, val); abort();
struct SomeResult {
enum Errors {
OK = 0,
ERROR_BAD_X,
ERROR_BAD_Y,
ERROR_BAD_Z
} error;
int value;
SomeResult(Errors error, int value): error(error), value(value) {};
SomeResult(int value): error(Errors::OK), value(value) {};
static func doSomething(int x, int y) -> SomeResult {
if (x < 0)
return { Errors::ERROR_BAD_X, 0 };
if (y < 0)
return { Errors::ERROR_BAD_Y, 0 };
return x + y ;
}
};
func myFunc(int z) -> int {
using enum SomeResult::Errors;
var x = z / 2, y = z * 3;
var [error, result] = SomeResult::doSomething(x, y);
switch (error) {
case OK: return result * result;
case ERROR_BAD_X: HANDLE_ERROR(x);
case ERROR_BAD_Y: HANDLE_ERROR(y);
}
};
int main() {
var result = myFunc(33);
printf("Ook, result: %d\n", result);
return 0;
}
При этом конкретные типы составных кортежей, как и типы для уже error и result задает не пользователь/вызывающий doSomething(), а разработчик doSomething, самому выводить эти составные типы в местах пользования не нужно. Разве не удобно?
При этом компилятор сразу тебя носом тыкает, разве это не прелесть?
./test.cpp:44:11: warning: enumeration value 'ERROR_BAD_Z' not handled in switch [-Wswitch]
./test.cpp:40:1: warning: non-void function does not return a value in all control paths [-Wreturn-type] 40 | };
Сможете такое повторить на "стандартном" С++, чтоб введение нового типа Exception сразу автоматически компилятором подсказало все места, где теперь нужно озаботиться о его обработке?
Про зло в виде switch default говорить не нужно, просто не нужно его использовать
Фундаментальная проблема пары [error, result]
- в том, что и error, и result существуют одновременно. Что вы там про статическую верификацию в соседней ветке говорили? Так вот, типы-суммы нужны чтобы обеспечить статическую верификацию по построению того факта, что result никогда не используется при наличии ошибки.
Что же до вашей программы - abort вызвать слишком просто, вы прокиньте код ошибки выше по стеку десяток раз, а потом рассказывайте как это "удобно".
Ну и макросы так писать нельзя, иначе потом безобидный код вроде вот такого будет содержать совершенно неочевидную ошибку:
if (error == ERROR_BAD_Y) HANDLE_ERROR(y);
Что же до вашей программы - abort вызвать слишком просто, вы прокиньте код ошибки выше по стеку десяток раз, а потом рассказывайте как это "удобно".
Но зачем по стеку десяток раз передавать наверх код ошибки? Вот чтобы что?
Оно конечно понятно, что типовой Java и не только код stacktrace подразумевает от 70 уровней вложенности вызовов, если меньше - коллеги засмеют, но а вот если подумать? Взять реальный код, и посмотреть, на сколько уровней выше нужно к примеру передавать код ошибок EAGAIN или EACCES?
Но хоть кто-то решил немного подумать и сразу написать вопрос (про как десять раз передавать), вместо того чтобы молча минусы выставлять (как же так, бубубу, мне же говорили, что коды ошибок и goto это очень, очень плохо, а exceptions хорошо и я точно буду молодец, если буду их пытаться использовать, а сейчас, сейчас просто молча заминусую, выскажу тем самым свое мнение, в интернете кто-то не прав!).
Ну и макросы так писать нельзя, иначе потом безобидный код вроде вот такого будет содержать совершенно неочевидную ошибку:
Недолго музыка играла - мозг увидел макросы (аааа, макросы же это плохо, говорили мне умные книжки и старшие, более опытные товарищи), они дают НЕОЧЕВИДНЫЕ ошибки.
А то что ни один более менее большой и рабочий проект без макросов не обходится - ой, какая неудобная правда.
Ладно, весело тут с вами, но хоть бы что-то новое и свежее услышать, вместо молчаливо негодующей массы минусующей, впрочем, видимо и не в этот раз.
У нас построена система на основе libmdbx, система на Python. Нам в Python надо было сигнализировать, что с kv произошли какие-то проблемы. Местами это было 7-8 уровней вложенности. Изначально все было написано на error codes (из libmdbx они заворачивались в свои), переход на исключения код заметно упростил.
Дополню: Те ошибки, которые мы можем исправить в моменте или которые являются частью логики, конечно оставлены кодами ошибки (перекодированными).
Дополню: Те ошибки, которые мы можем исправить в моменте или которые являются частью логики, конечно оставлены кодами ошибки (перекодированными).
В иронии выше про EAGAIN и EACCESS было просто два реальных сценария.
Первый - это вовсе не ошибка, а просто такой себе очень странный API в виде "ой, что-то пошло не так, попробуй еще раз тож самое повторить, на этот раз точно получится". Если подобное делать на Exceptions - то это лишь делает код нечитабельным, ну и может просадить производительность, хоть и не так чтоб сильно.
А второй класс ошибок это - "а блин, что-то в процессе пошло не так, шеф, все пропало, гипс снимает, клиент уезжает" - никакой дальнейшей обработки реально уже и не нужно предусматривать, нужно просто в scope текущего вызова управляемо освободить все внешние ресурсы, которые в процессе вызова были заимствованы (управляемо освободить менеджером внешних ресурсов, а никак не своим говнокодом), хендлы, аллокации памяти, вот это вот все, как-то оформить вызывающему сообщение о возникшей неожиданной проблеме, и на этом прекратить обработку вызова. Именно так себя ведут к примеру серверы баз данных внутри, и не только они.
И... и все.
Попытка же писать некую прикладную логику на exceptions - это просто закамуфлированное goto программирование, и не более того. В говнокоде допустимо, там не критично, в системах 24/7 (серверы баз данных, ядра ОС и т.п.) за подобный "стиль" - тебя просто не поймут.
Но в индустрии в какой-то момент что-то пошло не так, и уже вроде и 30 лет прошло, но нет, продолжают эти goto exceptions двигать в массы, и никакого Go с Дейкстрой и прочей функциональщиной на них не хватает. Настолько legacy изначально криво спроектированных прикладных сред придавило создание этих масс, что не видят очевидный обман и подмену понятий.
Вы каким-то удивительным образом разговариваете с голосами в своей голове, при этом цитируете мое сообщение.
как-то оформить вызывающему сообщение о возникшей неожиданной проблеме, и на этом прекратить обработку вызова
Для этого у нас, например, и используются исключения. И об этом я и написал.
Например, MDBX_PANIC - тут нам делать нечего и поправить мы не можем - бросаем исключение.
MDBX_DBS_FULL - тут мы можем что-то сделать, но в нашей реальности эти ошибка программиста и пользователь это никогда не увидит, впрочем как и обрабатывать на месте это не надо (но сообщить об этом надо и прекратить работу) - для этого тоже хорошо использовать исключения.
MDBX_NOTFOUND - тут мы можем что-то сделать в зависимости от нашей логики - заворачиваем в свой error codes и обрабатываем в моменте.
Простите, что приходится разжевывать, но судя по вашему сообщению надо.
Вы каким-то удивительным образом разговариваете с голосами в своей голове, при этом цитируете мое сообщение.
Ок, контекст держать уже не можем? До чего людей ChatGPT доводит в наше время, страшно подумать :) Ну да ладно, пусть это будут голоса в голове виноваты.
Например, MDBX_PANIC - тут нам делать нечего и поправить мы не можем - бросаем исключение.
И дальше что? Бросили, заллогировали, далее работаем как ни чем не бывало? Тут вообще-то в abort() выпадать надо.
MDBX_DBS_FULL - тут мы можем что-то сделать,
Кек лол, /** Environment maxdbs reached */ MDBX_DBS_FULL = -30791,
вы тут уже ничего не можете сделать, только рестарт процесса с исправлением конфига - т.е. читай выше про abort()
MDBX_NOTFOUND - тут мы можем что-то сделать в зависимости от нашей логики - заворачиваем в свой error codes и обрабатываем в моменте.
MDBX_NOTFOUND The specified database doesn't exist in the * environment and \ref MDBX_CREATE was not specified.
Тут тоже аборт и рестарт,
хотя вот тут да MDBX_NOTFOUND No matching key found.
Тут это просто даже не ошибка, а просто статус - нашли-не нашли, это не Exception ни разу, это банальное EXISTS() / NOT EXISTS(), просто Говарду Чу было проще это через код ошибки сделать, зачем-то.
MDBX_NOTFOUND - тут мы можем что-то сделать в зависимости от нашей логики - заворачиваем в свой error codes и обрабатываем в моменте.
Но забавно было конечно, вы там вообще чем занимаетесь? Точно программированием?
Такое комментировать только портить.
И дальше что? Бросили, заллогировали, далее работаем как ни чем не бывало? Тут вообще-то в abort() выпадать надо.
Зависит от того, насколько важная операция делалась на верхнем уровне, и есть ли резервная система.
Ну и, если речь идёт о сервере, ответ "сервер сломался, ждите пока починят" тоже кто-то выдавать должен. Так что да, никакого аборта.
В нашем случае десктопное приложение, пользователи не любят когда им делают аборт. Если нельзя работать с одним файлом, можно работать с другим.
Зависит от того, насколько важная операция делалась на верхнем уровне, и есть ли резервная система.
Ну и, если речь идёт о сервере, ответ "сервер сломался, ждите пока починят" тоже кто-то выдавать должен. Так что да, никакого аборта.
Ну ок, движок баз данных поймал панику, но мы продолжаем упорно делать вид, что если у нас микросхема памяти глюкнула или dangling pointer превратил всю память в кровавое месиво из мусора, то все нормас, т.к. нужно же дескать кому-то выдавать "сервер сломался, ждите пока починят". Кому же еще это делать, как не полуживому трупу с паникой. Про существование таких штук, как haproxy и прочие балансировщики и сервис мониторы - не, не слышали.
Но я так и так понял, что вы там точно не промышленным программированием занимаетесь. Забавны скорее плюсующие и минусующие, кто бы мог подумать, что это еще и массовое явление сейчас. Даже задумываться начинаешь, кого сейчас берут в IT по собеседованиям.
Ну да, ну да, конечно же. Если в каждом крупном проекте используются макросы - значит, с макросами всё в порядке. А если в каждом крупном проекте коды ошибок передаются выше - это всё потому что джависты разработчиков покусали, такого быть не должно.
stacktrace можно размещать не после данных исключения, а до структуры __cxa_refcounted_exception, размер которой не меняется. Только нужно как-то получить размер этой структуры. Или просто взять максимальный из всех поддерживаемых платформ.
Отличный вариант! Сделаю так для Boost. 1.88
Спасибо!
Только нужно как-то получить размер этой структуры.
Хм, … sizeof?
Настоящий холивар начинается вокруг необходимости переводить термины, а не транслитерировать их. Например, вместо "аллокация памяти" писать "выделение памяти". Транслитерация выигрывает у перевода в том случае, когда перевод занимает больше места, чем транслитерация, во всех остальных случаях проигрывает, и выглядит натужно.
Помогите нубасу найти в каком месте в llvm или gcc __cxa_allocate_exception
помечена как WEAK, а то нахожу только варинта без WEAK.
Грязные трюки C++ из userver и Boost