Обновить

Комментарии 183

то, что есть в тех компиляторах, которыми приходится пользоваться. Собственно, это и есть реальный С++. …

На самом деле скорее всего есть четыре, а не три типа С++. Четвертый вид С++идет идет следующим и называется “то, что реально пользуются в продакшене”. Обычно это возможности, которые отстают от третьего пункта на несколько поколений стандарта. Возможности, у кого известны все подводные камни и нюансы и есть наработана легаси практика.

А, нет. Есть еще и пяты тип С++ - это весь объем легаси кода, который тянет за собой обязательное требование по обратной совместимости со самыми старыми версиями и именно этоот пятый вид и тормозит развитие С++.

С++ без обратной совместимости это, наверно, C#.

Нет. Не тот уровень производительности.

когда в нативный код компилируется, уровень производительности не отличается. Но такой компилятор доступен, конечно, далеко не всем.

На самом деле нет.

Но это уже тонкости, доступные да, не всем.

На самом деле в C# ломающих изменений было, пожалуй, не больше чем в C++ (я сходу вспоминаю лишь два граничных случая, которые никогда не были в проде в исходном их виде).

А концептуальных расширений - примерно столько же.

Ещё C++ для embedded ;)

Типа в начале .cpp-файла указывается версия С++, для которой в этом файле пишется код. Но, к сожалению, эту идею в комитете похоронили.

Идея привязки кода к разным версиям стандарта существует до сих пор и частично прорабатывается в профилях безопасности. Просто это привязка реализуется не к нумерации стандарта С++, а к его возможностям, которые не связанных одной версией, что гораздо более правильно, как мне кажется.

Перед тем, как добавлять эпохи, надо быть уверенным, что для новых проектов вообще будут выбирать современный С++ хоть сколько нибудь часто, в чем я оооочень сомневаюсь.

Даже если на C++ в ближайшие 40 лет будут только сопровождать существующие проекты, то все равно выигрыш от эпох будет, т.к. сопровождение – это не только баг-фиксинг в уже написанном коде, но и дописывание новых фрагментов. И новые фрагменты лучше дописывать на более современном диалекте C++.

Новые фрагменты надо писать в стиле проекта, чтобы он легко "читался". Если ты в проекте видишь солянку от void* до вариадиков, это зло.

Так что я стою на своем: отдельные библиотеки, покрытые тестами, можно писать как душе угодно. Пока они хорошо работают, их все равно читать никто не будет. А новый С++ не заточен на читателей, он для писателей.

К сожалению, в больших проектах с большим временем жизни постоянно приходится возвращаться к написанному ранее - тут через три года безупречной работе в проде вдруг вылез какой-то баг (совершенно экзотический, никому в голову не приходило что звезды вот так встанут), тут бизнес-логика изменилась, тут с ростом объема данных производительности не хватает и надо что-то придумывать... И вот любой писатель, вынужден сначала быть читателем...

Я об этом и говорю. Выделяешь тонну времени, заново разбираешься, заново с матюками отлаживаешь "современный модный код", заново покрываешь тестами, опять не лезешь.
И все это только для того, чтобы почесать свое/комитета эго.

Новые фрагменты надо писать в стиле проекта, чтобы он легко “читался”.

Т.е. если проект стартовал в 1990-е и был написан в стиле “ООП головного мозга”, то в таком же стиле он и должен дописываться в 2030-ом?

Если он хорошо читается и удовлетворяет требованием производительности - почему нет?

Основная проблема в том, что порой чтобы понять что именно делает код из (условно) 100 строк вам надо продраться через 1000 (опять условно) строк другого кода, содержащего десяток слоев абстракций. И не дай бог, если проблема (дефект бизнес-логики, дефект производительности...) окажется где-то там...

Если он хорошо читается и удовлетворяет требованием производительности - почему нет?

Потому что прогресс не стоит на месте. Можно продолжать писать вложенные for-ы на страницу, а можно написать одну-две строчку на ranges.

Если эти 1-2 строчки будут работать быстрее - почему нет?

А если это потянет 5 уровней абстракций и кучу выделений-освобождений памяти в результате которых упадет производительность - точно нет.

Если эти 1-2 строчки будут работать быстрее - почему нет?

Потому что так думает тов. @satix

Новые фрагменты надо писать в стиле проекта, чтобы он легко “читался”.

И отправить этот код и все, что его хоть как-то касается, тестерам на перетест...

Как будто другие типы изменений не нуждаются в перетестировании…

Нуждаются, но, ведь, разговор идет о перписывание работающего, протестированного, устривающего по скорости кода на "новый С++" ради "прогресс не стоит на месте".

разговор идет о перписывание работающего, протестированного, устривающего по скорости кода на “новый С++” ради “прогресс не стоит на месте”.

Не знаю кто вел разговор об этом, но точно не я.

Я изначально говорил про дописывание новых фрагментов. Типа была софтина, которая интегрировалась с внешними системами S1, S2, S3, …, SN. Теперь появилась новая внешняя система S(N+1) для интеграции с которой нужно дописать новый модуль (или несколько).

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

Ну есть разница по объему и времени тестировании между регресс-тестом изменений и полным ретестом.

Более того, стиль проекта должен быть увязан с используемыми библиотеками.

На мой взгляд, одна из ключевых и пока что нерешенных проблем C++ в том, что в огромном количестве проектов исключения под запретом. Да, идея была крутой для конца 1980-х, однако испытания реальностью не выдержала.

Мне кажется, что исключения так и остаются самой крутой штукой С++ и выдержали испытания, так как их функционал присутствует во всех языках программирования, даже тех, кто от них якобы отказалася.

Но сложности с их использования действительно есть, но как правило это не проблема С++ как такового, а проблема интерфейса между компонентами, ведь extern “C” ничего не знает об исключениях. Поэтому не помешал бы какой нибудь дополнительный механизм их проброса исключений через интерфейсы.

но как правило это не проблема С++ как такового

Это проблема как только у вас исключения начнут возникать в hot path.

А кто вам мешает именно в этом месте сделать другой способ обработки ошибок, например на возвращаемых значениях?

Мешает то, что узнать о вылете исключения в hot path вы можете только постфактум. Т.е. написали, отладили, запустили в прод, спустя какое-то время обнаруживаете, что на некоторых специфических нагрузках у вас производительность падает на порядок. Выясняете и видите, что в этих нагрузках возникают исключения, о которых вы даже не подозреваете. Например, какой-то клиент в экзотических ситуациях шлет некорректные данные, разбор этих данных ведет к исключениям, исключения роняют вам производительность. Но до этого с такими некорректными данными вы не сталкивались.

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

Вы не ответили на заданный вопрос.

К тому же у вас всегда остается возможность просто не перехватывайте исключения. Тогда приложение будет сразу завершаться, что будет полностью соответствовать поведению Rust.

Вы не ответили на заданный вопрос.

Ответил. Я вам прям расписал случай из практики. Не единичный.

К тому же у вас всегда остается возможность просто не перехватывайте исключения.

У вас тоже есть возможность подумать и не писать глупости, но…

Приложение не имеет права "вот так просто завершиться". Любая ошибка должна быть зафиксирована и обработана. Для этого вы всегда должны четко знать где она случилась и почему - ошибка в программе, ошибка в логике, ошибка в данных...

Приложение не имеет права “вот так просто завершиться”

На самом деле все зависит от приложения. В некоторых случаях не только имеет, но и должна. Это т.н. принцип fail fast или let it crash.

Но не любое и не всегда.

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

Потому что когда на проде в фоне работает 100500 заданий и одно из них крешнулось, сопровождение сразу об этом должно знать и, возможно, предпринять какие-то действия. И должна быть фиксация что где и почему упало. Ибо если падение повлекло какие-то серьезные последствия для бизнеса, потом будет "комиссия по инцидентам" с расследованием ситуации.

Последствия креша могут быть на несколько порядков серьезнее чем "не успел сохранить в файле последние 5 минут работы".

Нет.

Отучаемся говорить за всех.

Потому что когда на проде в фоне работает 100500 заданий и одно из них крешнулось, сопровождение сразу об этом должно знать и, возможно, предпринять какие-то действия.

Когда на проде работает 100500*1000 заданий и 1000 из них каждую секунду крашатся из-за того, что общаются с внешним миром посредством разного качества инструментов, то это 1000 подробных отчетов в секунду нафиг никому будут не нужны.

Вы смотрите на мир из своего очень специфического угла, а этот мир очень и очень разнообразен. В нем есть и задачи, которыми занимаетесь вы (и для которых озвучиваемые вами требования актуальны), и еще туева хуча задач, о которых вы никогда и не слышали.

В том и дело что задания у нас крашатся в самых исключительных случаях.

А 99% случаев ошибки перехватываются и обрабатываются. Т если есть возможность продолжать работу - работа продолжется.

Условно говоря, если где-то случилось деление на 0 потому что в данных попался 0 там, где его быть не должно - ошибка будет зафиксирована подробно - "там-то и там-то некорректные данные - 0"

Аналогично - переполнение (у нас переполнение вызывает системное исключение).

А вот когда начинает систематически крашится какой-то конкретный модуль - тут уже сопровождение обычно откатывает последний патч (если началось после установки нового патча - с патчем всегда идет "план отката").

есть и задачи, которыми занимаетесь вы (и для которых озвучиваемые вами требования актуальны), и еще туева хуча задач, о которых вы никогда и не слышали

Уверен что так и есть. Хотя за... 35 лет в разработке много чем занимался в разных областях и много чего повидал.

В том и дело что задания у нас крашатся в самых исключительных случаях.

У вас – да. У других может быть сильно иначе. Поэтому не нужно на все смотреть через призму собственных задач.

Я смотрю сквозь призму опыта. Мне не кажется хорошим стилем просто крашить задачу без подробного объяснения причин в стиле "Что-то пошло не так...".

Для меня обработка ошибок всегда была более сложным местом, чем реализация логики.

Мне не кажется хорошим стилем просто крашить задачу

Повторю еще раз: для вас. Между тем есть целый подход к обспечению надежности под названиями fail fast или let it crash.

std::set_terminate ?

Разработчики Erlang с вами не согласятся ….

А так и делаем. Структурированная ошибка называется.

Беда в том, что в этом самом hot path практически все...

Во всех учебниках написано, что исключения не надо применять для реализации логики, только для обработки ошибок.

Используйте setjmp :адский смех:

Во всех учебниках написано, что исключения не надо применять для реализации логики, только для обработки ошибок.

А кто говорил о логике на исключениях?

Простейшая ситация: класс, конструктор которого получает int, но этот int не должен быть отрицательным. Грубо говоря, int-ы используются в вычислениях, в которых промежуточные значения могут быть отрицательными, но результат – либо 0, либо положительное. По результатам вычислений создается новый объект, который в конструктор int и получает.

class calculation_result {
  int _values_to_handle;
  ...
public:
  calculation_result(int values_to_handle, /* другие аргументы */)
    : _values_to_handle{ values_to_handle }, /* другие члены класса */
  {
    if(_values_to_handle < 0) throw std::runtime_error{ ... };
    ...
  }
  ...
};

И вот у нас в программе calculation_result создаются миллионами штук. Но на каких-то данных выявляется ошибка в наших вычислениях из-за которой values_to_handle все-таки отрицательный. Конструктор бросает исключение. Это исключение где-то выше ловится.

Все же это логика.

Тогда ошибка должна сформировался не на единичный объект, а на блок вычислений. Я бы так спроектировал

Все же это логика.

Для меня “логика на исключениях” – это все-таки другое. Например, когда принудительный выход из рекурсии делается пробросом специального исключения, которое где-то выше ловится именно как признак того, что рекурсия была специальным образом прервана.

И что вы можете сделать? Если у вас в данных бардак, и данные которые не могут существовать попали в логику?

Допустим у вас температура в кельвинах а вы там отрицательное значение передали. И что делать приложению?

И что вы можете сделать?

Бросить исключение.

Если у вас в данных бардак, и данные которые не могут существовать попали в логику?

Что значит “данные попали в логику”?

Допустим у вас температура в кельвинах а вы там отрицательное значение передали. И что делать приложению?

Это вы у меня спрашиваете? А почему у меня?

Я выше пример привел: по результатам вычислений создается класс, класс проверяет корректность переданных в него данные. Если данные не корректны, то порождается исключение. Некорректные данные могут появиться, например, в результате ошибок в коде (и появляются, что характерно). Пример, на мой взгляд, исчерпывающий. Если вам с ним что-то непонятно, то могли бы уточнить непонятные для вас моменты.

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

Делайте или статический фабричный метод или проверяйте данные не в конструкторе, а с помощью обычной функции. Другими словами, прерывания тут совершено не причем.

У вас класс принимает бизнес данные

Слова-то какие. А в std::vector::at тоже бизнес-данные поступают?

Делайте или статический фабричный метод или проверяйте данные не в конструкторе

Простите, вы всегда такой умный или только в разговоре со мной?

Показанная выше проверка в конструкторе – это, по сути, проверка контракта со стороны calculation_result. Он требует, чтобы ему давали уже проверенную информацию, но сам, ради паранои, делает дополнительную проверку. И, так уж получается, как последний бастион, ловит ошибки, которые a) не должны были бы возникать вообще и b) должны были быть проверены выше. Но не были, т.к. в коде случаются ошибки.

Вам уже написал, что это вы ССЗБ и нечего пенять на молоток при кривых руках.

Вам уже написал, что это вы ССЗБ и нечего пенять на молоток при кривых руках.

Льщу себя надеждой, что я не более ССЗБ, чем разработчики стандартной библиотеки, добавившие метод at в std::vector.

И что вы можете сделать? Если у вас в данных бардак, и данные которые не могут существовать попали в логику?

Контроль данных на входе, контракты.

По-моему вы используете исключения для обработки ошибок. Они нужны не для этого. Исключения нужны, извиняюсь, для исключительных ситуаций. Обычно это что-то связанное с IO. В вашем же примере вы их используете чтобы проверить “правильные ли данные мне передали”. Это должно быть обработкой ошибки, раз вы не доверяете тому кто ваам их передает. А еще лучше - используйте систему типов для предотвращения возможности таких ошибок, например unsigned.

По-моему вы используете исключения для обработки ошибок.

А метод std::vector::at использует исключения для чего?

Исключения нужны, извиняюсь, для исключительных ситуаций.

А получение методом std::vector::at неверного индекса – это исключительная ситуация или где?

А еще лучше - используйте систему типов для предотвращения возможности таких ошибок, например unsigned.

ЕМНИП, для целой кучи алгоритмов над матрицами и векторами удобно использовать именно знаковые индексы, потому что эти самые индексы нужно двигать туда-сюда. Настолько удобно использовать именно знаковые, что даже в стандартную библиотеку C++ была добавлена функция std::ssize. И если при обработке знаковых индексов программист допускает ошибку и эта ошибка не отлавливается тестами, а затем проявляется в работе, то конвертация из знакового в беззнаковый проблему не решает от слова совсем.

И да, байти про то, что нужно тестировать, рассказывать бесполезно.

А метод std::vector::at использует исключения для чего?

это нужно спрашивать у людей который его предложили в стандарт. Я его ни разу не использовал и плохо представляю ситуацию где это нужно.

А получение методом std::vector::at неверного индекса – это исключительная ситуация или где?

в моем понимании это должен быть ассерт (т.е. ошибка программиста), что и делает std::vector::operator[]. Но кто-то может использовать это и как исключительную ситуацию, если для него это подходящее поведение.

то конвертация из знакового в беззнаковый проблему не решает от слова совсем.

по-моему отлично решает. С беззнаковым вам компилятор говорит - иди проверь что у тебя правда беззнаковый и конвертируй. Т.е. программист будет обязан это сделать. Со знаковым он просто забудет об этом или будет уверен что не допустил ошибку в предыдущем коде и передаст как есть. Т.е. по сути контроль ошибки переходит из конструктора в код вызывающий его.

Я его ни разу не использовал и плохо представляю ситуацию где это нужно.

Зато я использую регулярно и не менее регулярно он ловит ошибки в коде. Более того, это настолько хорошо зарекомендовало себя на практике, что в C++26 будет т.н. hardering для стандартной библиотеки.

в моем понимании это должен быть ассерт (т.е. ошибка программиста)

И? Вот есть два программиста: Вася, который вычисляет индекс, и Ваня, который пишет реализацию std::vector. Вася допустил ошибку, в результате к Ване в vector::at приходит недопустимый индекс. И что делать Ване? Ведь ошибку допустил не он.

Классический assert работает только в DEBUG сборках, но превращается в ничто в RELEASE. Значает ли “ваше понимание”, что в RELEASE данные на входе в std::vector::at проверять не нужно?

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

Во-первых, по умолчанию не говорит. Во-вторых, даже когда в проекте форсят предупреждения на неявные преобразования, то зачастую программисты просто подавляют такие предупреждения static_cast-ом без реальных проверок.

Кроме того, у меня складывается ощущение, что вы придерживаетесь той точки зрения, что код можно протестировать и найти/устранить все допущенные ошибки в ходе этого процесса. Грубо говоря “пиши правильно и ошибок у тебя не будет”.

Увы, это не работает на практике.

Более того, это настолько хорошо зарекомендовало себя на практике, что в C++26 будет т.н. hardering для стандартной библиотеки.

насколько я знаю, std::vector::at не имеет ничего общего к hardening. Hardening это как раз ассерт в std::vector::operator[], а не исключение в at.

Зато я использую регулярно

Зачем вы его используете? В какой ситуации?

И что делать Ване? Ведь ошибку допустил не он.

Ваня должен декларировать интерфейс - возвращает элемент если индекс корректный и, например, UB если неккоретный. Но исключение тоже выглядит как валидный вариант (std::vector::at). Так же выглядит и возврат std::optional. Это лишь вопрос реализации.

Значает ли “ваше понимание”, что в RELEASE данные на входе в std::vector::at проверять не нужно?

В at нужно, потому что в этом его смысл - таким его придумали и стандартизировали. В operator[] - не нужно. Или, если точнее, человек который компилирует/пишет код может принять решение что делать в случае assert - C++26 контракты позволяют выбрать вариант поведения. Того же эффекта можно добиться и реализацией своих ассертов.

то зачастую программисты просто подавляют такие предупреждения static_cast-ом без реальных проверок.

Ну если мы пришли к тому что не верим в добросовестность своих же коллег, то нас уже ничто не защитит против *(int*)0xDEADBEEF

… что код можно протестировать и найти/устранить все допущенные ошибки в ходе этого процесса. Грубо говоря “пиши правильно и ошибок у тебя не будет”.

Нет, скорее придерживаюсь взгляда, грубо говоря, “пиши правильно и ошибок у тебя не будет, а если будут - сделаешь хотфикс".

Но я не уверен что это верно. Не уверен что так можно создавать надежный софт. Я довольно неопытен в разработке ПО.

Увы, это не работает на практике.

а что работает?

насколько я знаю, std::vector::at не имеет ничего общего к hardening

Зато hardering имеет отношения к тому, что в интерфейсе vector (и не только) сперва сделали operator[] без проверки (т.е. полное доверие разработчику) и at с проверкой. Время показало, что доверие разработчику – это так себе подход, поэтому сейчас и для operator[] делают принудительную проверку.

Hardening это как раз ассерт в std::vector::operator[]

Во-первых, это уже детали. Во-вторых, это не унаследованный из Си assert, а разновидность контракта, который нельзя просто так проигнорировать. В-третьих, как уже сказал, принципиальна принудительная проверка в operator[].

Зачем вы его используете?

Для доступа к содержимому вектора или std::span.

В какой ситуации?

Когда нет уверенности в корректности имеющегося на руках индекса. Например, когда индекс приходит из вне. Или вычисляется.

Хотя временами от at приходится отказываться по показаниям профайлера.

Ваня должен декларировать интерфейс - возвращает элемент если индекс корректный и, например, UB если неккоретный.

Опять же, как показала практика, UB в качестве разновидности “интерфейса” – это так себе подход. Так что в Ж такие интерфейсы, как и разговоры об их допустимости (за редким исключением).

А если мы UB как нормальный вариант не рассматриваем, то мы в ситуации когда:

  • есть явное нарушение контракта со стороны клиента нашего класса/метода/функции;

  • явное нарушение контракта может быть исключительной ситуацией в ряде случаев (конструктор, перегруженный оператор, результат которого не может быть представлен условным std::optional/std::expected /в качестве примеров: operator[], operator* (для разыменования умного указателя)/). И если это исключительная ситуация, то использование исключения для информирования о ней – это нормально.

Кроме того, выброс исключения – это не есть “обработка ошибки” в данном случае.

Но исключение тоже выглядит как валидный вариант

Ну вот выше весь сыр бор начался с того, что кому-то не понравился выброс исключения из конструктора calculating_result.

Ну если мы пришли к тому что не верим в добросовестность своих же коллег

Здесь нет вопроса веры. Здесь есть факт того, что программисты ошибаются. Всегда. Даже когда они очень добросовестны. Особенно когда не очень.

Это данность. Для борьбы с которой придумывают и разные формы тестирования, и разные формы анализа, и проверки в ран-тайм.

Нет, скорее придерживаюсь взгляда, грубо говоря, “пиши правильно и ошибок у тебя не будет, а если будут - сделаешь хотфикс".

Звучит как “нужно делать как надо, а как не надо делать не нужно” ;)

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

а что работает?

Паранойя. Т.е. кроме тестирования (и разных форм статического/динамического анализа, если у вас есть возможность их использовать) нужны проверки в ран-тайме. Насколько вам позволяют требования по производительности и ресурсоемкости.

Грубо говоря, лучше начинать с использования std::vector::at, отказываясь от него только после проведенного тестирования и только в местах, где есть явная просадка производительности из-за проверок внутри at.

Любят люди для индексов использовать size_t, а по стандарту там должен быть знаковый ptrdiff_t…

По стандарту std::vector::operator[] получает индекс в виде типа size_type, который берется из аллокатора и, в большинстве случаев, является size_t.

Видимо у нас случилось недопонимание. Под массивами я понимаю классические "сырые" массивы, для них операция индексации является сахаром для адресной арифметики: <address> + <ptrdiff_t>. std::vector -- не массив (хотя внутре у ей неонка), а инстанс класса с операцией индексации, заданной для size_type. Можно углубляться, но суть такая. И инварианты у него таковы, что отрицательные индексы бессмыслены.

Видимо у нас случилось недопонимание.

Скорее всего. Я говорил про std::vector.

ssize_t все-таки для сишной традиции отрицательными значениями возвращать ошибки.

Не только. ssize_t и знаковые индексы удобны для прохода по вектору в обратном направлении.

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

Именно об этом и говорю. Исключение - это непредусмотренная и необработанная ошибка.

А еще лучше - используйте систему типов для предотвращения возможности таких ошибок, например unsigned.

Если бы все было так просто. Ошибки в данных не ловятся типами.

Простейший пример - у вас есть сумма в валюте счета. Все суммы в коммерческих расчетах имеют тип с фиксированной точкой и хранятся в миноритарных единицах. Пусть это будет decimal(23, 0) (23 знака, 0 после запятой).

А теперь вам надо получить эквивалент суммы в валюте счета в рублях. И положить ее в переменную того же типа - decimal(23, 0)

Т.е. вам надо сконвертировать сумму в валюте счета в рубли по текущему курсу и проверить не превышает ли она заданное значение. Берем сумму в валюте счета, берем код валюты, берем из справочника курс, перемножаем и... Получаем системное исключение - "MCH1210 - RECEIVER VALUE TOO SMALL TO HOLD RESULT". Ну система так работает - она для коммерческих вычислений, тут переполнение является ошибкой.

Варианта два - оставить все как есть, ловить исключение и обрабатывать. Но это нехорошо с точки зрения производительности. Значит надо использовать временну. переменную с максимальным размером - decimal(63, 0). Конвертировать в нее, потом проверять, поместится ли она в целевую и если нет - возвращать уже структурированную ошибку. Примерно так:

dcl-s sourseCurrensy  packed(23: 0);
dcl-s targetRoubles   packed(23: 0) inz(*hival);
dcl-s cource          packed(23: 0);
dcl-s intermediate    packed(63: 0);

intermediate = sourseCurrensy * cource;

if intermediate > targetRoubles;
  error = ...
else;
  targetRoubles = intermediate;
endif;

targetRoubles инициализируется как *hival - максимально возможное значение для данного типа, в данном случае 23 девятки.

В данном случае если присвоение вызовет переполнение, возвращаем результат как максимально возможный для данного типа + ошибку (предупреждение) что на самом дел там больше.

Если возможны как положительные, так и отрицательные значения

if intermediate in %range(%loval(target): %hival(target));
  target = intermediate;
else;
  error = ...
endif;

%loval/%hival тут функции, возвращающие минимально/максимально возможные значения переменной.

В любом случае, такая проверка по потреблению ресурсов процессора обходится значительно дешевле чем исключение.

Это очень простой пример. Есть более сложные, когда данные, сами по себе валидны, но не проходят в разрезе бизнес-логики отдельно взятого процесса.

Ошибки в данных не ловятся типами

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

Вам лишь нужно объявить тип, который определяет свои арифметические операции, операции перевода в другую валюту.

Я связан контрактом. В БД все суммы хранятся в формате decimal(23,0) которого хватает в 99.99% случаев (за исключением когда на тестовом сервере накидали каких-то совсем нереальных данных). И я не могу вернуть какой-то иной тип.
Я просто показал что вместо перехвата исключения

monitor;
  targetRoubles = sourseCurrensy * cource;
on-excp 'MCH1210';
  targetRoubles = %hival(targetRoubles);
  error = ...
endmon;

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

Есть много других примеров, просто там все более сложно и долго расписывать.

На одной из моих работ мы искали самый удобный вариант реализации обработки ошибок. Долго обсуждали, тестировали варианты, пришли к некому решению. Наше решение было такое: всем методы, которые по логике должны вернуть результат типа result_t возвращают шаблонный класс Result<result_t>. В этом классе примерно как в std::tuple идет результат (валидный только если нет ошибки), код ошибки, и прочие метаданные ошибки (текстовое описание, …). Помимо этого был реализован макрос CALL(…) который вызывает функцию, и если она вернула ошибку - возвращает ошибку дальше через return, а если она вернула результат - возвращает его (как rvalue). И похожий макрос TRY(…) для void. Важно: возможность не инициализировать переменную а выйти из функции returnом в процессеэто расширение gcc а не возможность c++20, но мы использовали. Применение выглядело так:

Result<result_t> MyClass::do_someting(ArgType1 const arg1, ArgType2 const & arg2) {
    if (Consts:ArgType1ImpossibleValue == arg1) {
        return Result::error(RESULT_INVALID_VALUE, "Запрещенное значение");
    }
    TRY(chechAlive(arg2));
    auto v1 = CALL(this->prepareArg(arg1));     
    auto v2 = CALL(agr2.calculateFor(v1));
    result_t result = TRY(this->calculate(arg1, v2));
    return result; // вызовет неявное преобразование типа и вернет резултат
}

Наша реализация CALL и TRY в том числе ловила ексепшны (не потому что мы их кидали а потому что они могут кидаться std библиотекой)

Неудобно было использовать этот инструметнтарий в конструкторе, но обойти можно так:

static Result<void> MyClass::validate(ArgType3 const arg3) {
    if (Consts::ArgType3Limit > arg3) {
        return Result::error(RESULT_INVALID_VALUE, "Слишком маленькое значение");
    }
    // Result<void> без ошибки вернется автоматически
}

static Result<MyClass> MyClass::safe_construct(ArgType3 const arg3) {
    TRY(validate(arg3));  //Валидируем
    return MyClass(arg3); // вызываемко конструктор без валидации внутри
}

Спасибо за развернутый и интересный комментарий.

Но у меня складывается ощущение, что это наглядный пример того, как из C++ пытаются сделать какой-то другой язык подручными средствами.

Ну то есть подход как в Rust только без удобств.

И даже там в итоге не все так просто и многие приходят к решению упростить через https://docs.rs/anyhow

Сегодня можно вместо Result вернуть std::expected.

Я так понимаю над обработкой ошибок вы особо не думали, и полиморфное поведение не используете.

Что конкретно вы понимаете под обработкой ошибок?

У нас сфера применения - сетевая ось. Наше решение имеет такие сильные стороны:

  1. Макросы единообразно вызывают функцию, проверяют результат и логгируют, какой вызови с какими параметрами произошла ошибка.

  2. Использование макросов сокращает код и делает его более читаемым.

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

А что касается обработки ошибок, у нас есть 3 основных типа источников ошибок:

  1. Наши ошибки в коде.

  2. Проблемы SDK чипов (баги и странное поведение).

  3. Проблемы в железе. В итоге если что-то не получилось, то надо убрать за собой (высвободить все что выделил) и попробовать работать дальше. И запись в логах нужна, но плохо, если повторные попытки засыпают лог ошибками.

Есть еще валидация входящих данных, но она особняком стоит. Мы всегда вначале пом максимуму валидируем данные, а потом все конфигурируем.

А что вы понимаете под полиморфыным поведением?

Под полиморфным поведением конкретно здесь я понимаю полиморфизм в стиле ООП.

Если у вас есть открытая иерархия типов с виртуальными методами, то невозможно в сигнатуре метода в базовом типе указать все возможные отказы, которые могут возникнуть в любом (будущем) потомке. Но эту проблему как раз решают исключения.

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

PS: В Java вон Checked exceptions где сейчас?..

Если у вас есть открытая иерархия типов с виртуальными методами, то невозможно в сигнатуре метода в базовом типе указать все возможные отказы, которые могут возникнуть в любом (будущем) потомке.

Такая штука разруливается чем-то вроде std::expected<Ok_type, std::error_code>. Наследники могут делать свои категории ошибок.

Если же требуется распространять вместе с ошибкой какие-то дополнительные данные, то можно делать что-то вроде:

class error_base { ... };
using error_unique_ptr = std::unique_ptr<error_base>;

class some_interface {
public:
  [[nodiscard]] virtual
  std::expected<Ok_type, error_unique_ptr>
  some_method() = 0;
  ...
};

Наследники могут делать свои производные от error_base типы.

Но да, удобство всего этого под вопросом. Однако, эти способы гарантируют большую предсказуемость, чем исключения.

Получилось ничто иное, как базовый класс Exception, только свой. А зачем?

Затем, что здесь полностью все в ваших руках и вы можете предсказать сколько времени займет прокидывание ошибки наверх. Тогда как с исключениями об этом можно только гадать.

Насколько я понял, в вашем случае не происходит Stack Unwinding, поиска обработчика, не требуется создания LSDA.

Т.е. тут по определению ощутимо меньше накладных расходов.

В нашем случае мы пользуемся "структурированной ошибкой" - ошибка заполняется в структуру

 typedef struct Qus_EC
    {
       int  Bytes_Provided;
       int  Bytes_Available;
       char Exception_Id[7];
       char Reserved;
       char Exception_Data[];
    } Qus_EC_t;

Но это уже определяется поддержной на уровне самой системы - тут есть т.н. файл сообщений (message files) где хранятся все ошибки - коды, текстовки, уровень серьезности и т.п.

MCH0601 - код сообщения - поле Exception_Id

&1, &2 и &3 - места для подстановки конкретных данных (которые передаются в поле Exception_Data)

И есть системное API

 void QMHRTVM (void *,           /* Message information            */
               int,              /* Length of message information  */
               char *,           /* Format name                    */
               char *,           /* Message identifier             */
               void *,           /* Qualified message file name    */
               void *,           /* Message data                   */
               int,              /* Length of message data         */
               char *,           /* Replace substitution values    */
               char *,           /* Return format control          */
               void *,           /* Error Code                     */
               ...   );          /* Optional parameter group:
                                      Retrieve option
                                      Convert to CCSID
                                      Message data CCSID           */

Которое вернет (при необходимости) полный текст сообщения с подставленными данными и уровень серьезности (0 - информационное, 10 - предупреждение, 20 и выше - ошибка).

Можно создавать свои файлы сообщений, создавать свои типы сообщений.

При обработке ошибки обычно достаточно просто ее кода.

А принципе, такое несложно реализовать на уровне библиотеки.

Более того, мы часто используем более краткий формат структуры - 7 символов код ошибки + 3х10 символов параметры. Этого вполне достаточно.

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

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

Т.е. тут по определению ощутимо меньше накладных расходов.

Принципиальный момент не в “меньше” или “больше”. А в том, что главная претензия к существующим C++ным исключениям в таких областях как реальное время – это непредсказуемость операции выброса исключения. Т.к. все это находится под контролем у компилятора и стандартной библиотеки, то разработчик написав throw не знает к чему именно этот самый throw приведет: где и как будет создан экземпляр исключения, куда будет передано управление, сколько займет поиск подходящего catch и пр.

В случае с явным возвратом объекта вместо выброса исключения накладных расходов, на самом-то деле, может оказаться даже больше. Но зато эти расходы могут контролироваться самим разработчиком. Например, он может сам управлять тем, где и как будет создан экземпляр “исключения” (и не обязательно это может быть динамическая память, которая не предсказуема по определению). Кроме того, стоимость возврата условного std::expected с конкретными типами вполне себе можно оценить и эта стоимость вряд ли будет “плавать” в зависимости от фазы луны компилятора, стандартной библиотеки, текущего состояния динамической памяти и пр. факторов.

Да, все верно

 в таких областях как реальное время – это непредсказуемость операции выброса исключения

В RT системах - да - нарушение гарантированного времени отклика. В HiLoad - ресурсы.

В случае с явным возвратом объекта вместо выброса исключения накладных расходов, на самом-то деле, может оказаться даже больше

Наверное, зависит от системы в целом, но по нашим PEX (Performace EXplorer) статистикам Stack Unwinding - очень дорогая штука с точки зрения ресурсов процессора...

Мы даже динамическую память стараемся по минимуму использовать - даже это для нас дороговато.

Структура ошибки фактически получается одна - на стеке верхнего уровня. Дальше она уже передается вниз по ссылке.

Ну бывают редкие ситуации когда нужно собирать историю неблокирующих ошибок (предупреждений). Тогда делается "стек ошибок" из таких структур. Тоже один и по ссылке передается сверху вниз с постепенным заполнением.

А является ли эта непредсказуемость (надо сказать, очень условная) действительно проблемой?

Почему оптимизация хранения коротких строк в std::string к примеру не является такой же проблемой?

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

А является ли эта непредсказуемость (надо сказать, очень условная) действительно проблемой?

Насколько я знаю, действительно является и действительно проблемой. В проектах, связанных с реальным временем, исключения под запретом. Как и работа с динамической памятью после начальной инициализации.

То же самое относится и к высоконагруженным системам с высокими требованиями к производительности и эффективности использования ресурсов процессора.

Да, не влезет. Да, через стек. Но эти накладные расходы (на передачу дополнительного параметра) ничтожны по сравнению с расходами на Stack Unwinding которые неизбежны при исключении.

У Вас что, исключения используются для Control Flow, что исключения на каждом шагу?

Накладных расходов меньше на пути, когда ошибка возникает. На нормальном пути - больше.

А ещё ментальная сложность при поддержке этого дела и при отсутствии дисциплины - возникновение желания что-нибудь "неважное" отрезать.

Накладных расходов меньше на пути, когда ошибка возникает.

Так с точки зрения предсказумости и контроля со стороны разработчика это и является ключевым.

А ещё ментальная сложность при поддержке этого дела и при отсутствии дисциплины - возникновение желания что-нибудь “неважное” отрезать.

Поэтому я и сказал выше “Но да, удобство всего этого под вопросом.”

Так с точки зрения предсказумости и контроля со стороны разработчика это и является ключевым.

Давайте сразу оговорим, что это с точки зрения задач, с которыми Вы работаете и здесь неявно подразумеваете.

Это не обязательно справедливо для любого класса задач.

Давайте сразу оговорим, что это с точки зрения задач

Давайте я еще раз сошлюсь на текст статьи и акцентирую внимание на том, что существующие исключения разделили мир C++ на два лагеря: в одном исключения под строгим запретом (основные причины: ресурсоемкость /недопустимо для низкоуровневого системного программирования/ и непредсказуемость /недопустимо для реального времени и высоких нагрузок/), в другом исключения используются.

Это разделение, имхо, пагубно влияет на экосистему. Например, библиотеки и фреймворки, сделанные в одном лагере, могут оказаться неприменимы во втором. Что есть плохо. Даже совсем плохо.

Соответственно, моя точка зрения состоит в том, чтобы объединить эти два лагеря добавив в язык исключения, которые будут нести предсказуемые накладные расходы. Использование этих исключений будет похоже на то, что обсуждается в данной ветке (начиная, как минимум, с этого комментария). И это вполне себе рабочий способ. Не без недостатков, но уж точно лучше, чем когда исключения совсем под запретом. Или когда исключения разрешены, но кто-то решается сделать throw -1;

А что вы подразумеваете под "любым классом задач"?

Системы реального времени исключили. Ну ок. Высоконагруженные системы исключили. Тоже ок. И что остается? Мелкие утилики? А не слишком ли сложный язык вы выбрали для написания утилиты из 300-500 строк кода?

Идея ясна, но от идеи прокидывать исключение наверх через слои абстракции, авось кто поймает, мы отказались. Хотя бы по той причине что если что-то пошло не штатно, то промежуточные слои должны откатить назад то, что начинали делать, и это не всегда покрывается RAII. А фактических кодов ошибки используемых у нас даже меньше чем в errno.

Если исключительные ситуации возникают часто, то это не исключительные ситуации. Внезапно да, у слова "исключение", оказывается, есть смысл.

Если исключительные ситуации возникают часто

Осталось определиться с понятием “часто”.

Увы, но использование исключений катастрофически отрицательно влияет на производительность... Вплоть до того, что на нагрузочном тестировании вам вынесут приговор - "20% ресурсов процессора уходит на работу с исключениями - переделать".

Исключение допустимо в условиях возникновения критической, блокирующей ошибки. Когда "ой, все..." - дальнейшая работа невозможна. Но не как средство возврата обрабатываемой ошибки, подразумевающий какую-то дальнейшую логику.

Иииии…?

Я не требую использовать исключения в качестве единственного варианта обработки ошибок. Я пишу про то, что в определенных случаях исключения очень удобны, а прерывание выполнения потока команд так или иначе присутствует во всех языках программирования, даже в тех, у которых исключения якобы отсутствуют.

Поэтому лучше иметь стандартизированные исключения с возможностью их обработки в рантайме, чем в каждом случае завершать выполнение приложения по abort или с помощью хаков пытаться реализовывать перехват подобных ошибок.

Я не спорю. Больше скажу - удобнее всего когда исключения не языковые, а системные. С возможностью пробрасывать их на любой уровне стека вызовов (вплоть до за пределы своей программы тому, кто ее вызвал с уверенностью что там оно тоже будет перехвачено и обработано вне зависимости от того, на чем та программа написана).

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

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

Как пример - обработка 25 000 000 записей в рамках заданного временного окна и условиях ограничения ресурсов. В 10-ти записях (условно, может быть 100, может быть 1000) возникает какая-то ошибка, связанная со специфическим набором данных в конкретной записи (скорее всего, какое-то количество таких ошибок будет всегда). Такие ошибки необходимо фиксировать для последующего ручного разбора. Но все остальные записи надо продолжать обрабатывать. Вот тут исключения категорически непригодны т.к. снижают производительность.

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

Так именно это я и имею ввиду. Исключения нельзя использовать для обработки часто возникающих ситуаций. Их назначение - прерывать поток выполнения при возникновении не восстановимой ситуации, когда становится не важна производительность и требуется корректно обработать завершение приложения. Но если на более высоком уровне есть механизм обработки подобных ситуаций, то пусть он перехватывает исключения и продолжает работу приложения дальше (например вот так Избавляемся от ошибок Segmentation fault из-за переполнения стека в С++).

Другими словами, исключение, это не обработчик ошибок, а возникновение события достаточного для завершения работы всего приложения, но с возможностью его обработки на более высоких уровнях.

Другими словами, исключение, это не обработчик ошибок, а возникновение события достаточного для завершения работы всего приложения

Выделено в корне неправильно.

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

Зачем развивать то, что должно быть разделено на несколько различных языков? Да, у таких языков будет похожий синтаксис, но задачи будут решаться различные. Например, "С++ с указателями", "С++ без указателей"; "С++ с модулями", "С++ без модулей"; ... Для программирования пользовательского интерфейса нужно что-то своё, для работы с БД — что-то другое. Зачем всё грести в один монструозный язык, который и за всю жизнь не изучишь?

И каждого варианта свои стандарты, свои компиляторы, возможно свои особенности синтаксиса … И все это должно иметь возможность взаимодействия между сбой. Как представлю, бррр… :-(

Фактичекски это уже разделение на разные языки от одного корня. И под каждую задачу будет выбираться тот, который наилучшим образом ей соответствует. И за совместимость тут не думаю что надо так уж переживать...

не совсем. а что такое "C^^" и "D"?

C^^ это С++/CLI, если полностью, там указатели - крышечки.

D это dlang

Комитет должен отойти от практики публикации опережающих реальность спецификаций. Вместо того, чтобы декларировать свои фантазии …

Мне кажется, что в С++26 включили рефлексию и контракты, просто чтобы снизить градус критики комитета. Как будто подкинули пару задач на ближайшие три года, чтобы не заниматься безопасностью и действительно важными вещами.

Я не хочу сказать, что рефлексию и контракты вещи “не важные”. Важные, вот только статическая рефлексия типов в текущем виде напрашивалась пару десятилетий назад, а контракты предложены в кокой-то непонятной (для меня) форме.

Было бы понятно, если бы контракты были аналогичны реализованным в Ада как элемент доказательно программирования во время компиляции для сужения значений у типов данных. Но если я правильно понял, принятый в С++26 вариант контрактов не вводит сужения значений для типов, а реализует обычные проверки условий, в том числе и в рантайм, что делает их обычной комбинацией static_assert и обычного assert. И стоило из-за этого городить огород?

В целом, да.

Ну, почти... Это ассёрты с кастомизацией поведения. Да, не то, о чём вы говорите, но и не совсем то же самое, что было.

Ох как согласен...

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

И Вы не зря упомянули PL/I - он рухнул именно по причине непомерно возросшей сложности. С++ уверенно идет туда же и держится на плаву исключительно за счет накопленной огромной кодовой базы.

Сам я начал писать на С в... 89-м, по-моему, году. Потом был "С с классами" (который очень нравился), потом С++, который нравился чем дальше тем меньше. И так до 17-го года, когда волею судеб занесло на достаточно экзотическую у нас платформу со своим стеком (да, С/С++ тут тоже есть, но это не основной язык) в разработку большой системы с высокими требованиями к надежности и производительности (АБС крупного банка).

С точки зрения накопленного здесь опыта вижу следующее - в С++ слишком много работы с динамической памятью. А это всегда накладные расходы (все это сразу видно на нагрузочном тестировании). Приходится извращаться с аллокаторами и посильно минимизировать использование динамической памяти.

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

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

Кстати а кто нибудь знает подробности почему "эпохи" похоронили?

Я подобную идею предлагал уже давно (в комментариях на Хабре). Кажется это было бы идеальным решением. И компиляторы долгое время все равно поддерживали бы все эпохи, но хотя бы можно было бы не беспокоиться за чистоту кода в конкретном файле: если в начале указана некоторая эпоха - можно быть уверенным, что там нет определенных языковых конструкций.

Кстати а кто нибудь знает подробности почему “эпохи” похоронили?

Подробностей не помню, но вроде как основным препятствием был фактор ABI, мол, модули, которые будут собираться под новую “эпоху”, могут требовать более свежую stdlib, чем модули, которые собираются под старую эпоху.

Вот здесь есть следы обсуждения: https://www.reddit.com/r/cpp/comments/gnw1pa/comment/frcet52/

А чего не хватает используя /std=c++2a например?

Не понял вопроса. Могли бы развернуть?

Ключик форсирует правила соответствующего стандарта при компиляции единицы трансляции.

Т.е устаревшее станет запрещенным.

Возможно, конечно просто надо это поведение компилятора ужесточить относительно текущего, сам как то не применял в обратную сторону(не разрешить новое, а запретить старое)

Я так понимаю, что логика комитета состоит в том, что старый код должен компилироваться новыми стандартами если в этом коде нет того, чтобы было явным образом удалено из языка. Грубо говоря, если был код, написанный в С++98 без std::auto_ptr, то этот код должен продолжать компилироваться и в С++11, и в C++17, и в С++26.

Но мне хочется иметь возможность убрать из языка какие-то вещи. Например, неявные преобразования типов. Однако, тогда старый код перестанет компилироваться условным C++29. И что делать, когда есть куски кода, написанные на C++17, который никто в обозримом времени переписывать не будет?

Можно, наверное, в проектном файле указывать что для вот этих .cpp-файлов стандарт C++17, для вот этих C++26, а для вот этих C++29. Но, как по мне, это так себе идея.

ИМХО, лучше для всего проекта выставить, скажем, С++17, а внутри конкретных .cpp-файлов писать

#pragma std=c++29

C++ помогли бы

  1. editions и отрезание deprecated. Хотя не уверен, удастся ли отрезать неявные преобразования типов - врождённый дефект языка для совместимости с Си. Уронили его в детстве с лестницы, последствия удара головой.

  2. Нормальный менеджер пакетов вроде cargo

Тогда может быть лет через 10 станет нормальным языком.

Нормальный медленно пакетов вроде cargo

Нормальный менеджер пакетов сейчас рождается в конкурентной борьбе между vcpkg и conan.

Неявные преобразования типов можно убрать прямо сейчас. Для собственных типов используйте explicit, кроме того можно отключить на уровне компилятора такое поведение.

А вот свой аналог cargo это вот то что реально нужно языку прямо сегодня.

Неявные преобразования типов можно убрать прямо сейчас.

Что-то зашито в ДНК языка. Например, какой результат будет у сложения двух std::uint8_t? https://godbolt.org/z/o44scjv47

Да, именно так - зашито в ДНК. Вы не знали про integer promotion описанное в стандарте? Но если вы знаете про это и это мешает вам вы вправе отключить такое поведение и исправить возникшие ошибки при сборке. Делов на 20 минут, начать и кончить :D На мой взгляд это не делает язык хуже или как то сложней. К тому же implicit приведение типов весьма полезная вещь, а компилятор подскажет про потерю точности если таковая будет. Вы же надеюсь читаете выхлоп компилятора? Я читаю.

К тому же все это нытье про сложность C++ напоминает мне нытье неосиляторов которые споткнулись о собственное не знание инструмента которое им ударило по лбу. Как говорил мой лётный инструктор - что бы не было залетов изучай район полётов.

Но если вы знаете про это

то это все равно не мешает совершать ошибки

вы вправе отключить такое поведение

каким образом?

Вы же надеюсь читаете выхлоп компилятора?

Что вы понимаете под выхлопом компилятора?

то это все равно не мешает совершать ошибки

Как будто тот же Rust не позволяет совершать ошибки...

каким образом?

Вас в гугле забанили?

explicit конструкторы классов и семейство флагов компилятора -Wconversion переведенные в ошибки через -Werror=. Аналогично для msvs W3 и W4 семейство флагов так же переведенные в статус ошибки.

Что вы понимаете под выхлопом компилятора?

Вам в output вашего редактора или ide компилятор пише что он там собирает, диагностические сообщения какие проблемы если не собралось. Не видели разве такого? Там еще бывают сообщения о потери точности при преобразовании типов если таковые включены.

Крайне рекомендую читать документацию к компилятору. Откроете для себя много нового.

Вас в гугле забанили?

Я вам пример на godbolt привел. Вот он же с вашими ключиками -Wconversion и -Werror: https://godbolt.org/z/1e4PEboxM Хотелось бы увидеть ошибку от компилятора.

Вам в output вашего редактора или ide компилятор пише что он там собирает, диагностические сообщения какие проблемы если не собралось.

Некоторые под выхлопом компилятора понимают не сообщения об ошибках или предупреждениях, а генерируемый компилятором ассемблерный код.

Честно говоря ошибки этого класса достаточно редки.

Я бы тогда уж смотрел на артефакты плавающих вычислений

Честно говоря ошибки этого класса достаточно редки.

Хуже всего то, что о таких неявных преобразованиях многие даже не знают. А из тех, кто знает, многие не помнят.

Гнать из С++ программистов ср...ой метлой, как говорила моя бабушка, если они этого не знают. Ну реально, как этого базового факта можно не знать???

Чтобы потом разгребать проблемы, когда разработчика пакета хакнули, и начали распространять через менеджер пакетов вирусы?)

Ну да, ну да, весь мир с этим как-то живёт, а вот C++ такой особенный, что только здесь будут проблемы

Если бы мы его понимали, но мы его не понимаем...

а я бы хотел язык такой стандарт языка С++(я понимаю что это почти невозможно), который схож с растом, суть в том, что в моём представляемом не существующем языке, конструкции языка типо нумерик/концепт/...всё что режет глаз элегантно как-то убрано в кишки языка и пускай язык сам решает в момент компиляции где там что, потомучто нагромождения кишок где что создаётся иногда выглядит как битва за то чтобы писать чисто, и мешает этому не только легаси, но и новшества которые надо в коде буквально как маркер описывать, и вот код уже около нечитаемый(важный нюанс чтобы были правила предсказуемости выведения это не копия раста, но типо синтаксис С++ его все фишки, но они в кишках выводятся сами правилами какими-то)

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

текущий С++23/26 мне нравится, но я уже присматриваюсь всё чаще на Раст, он как-то более читабельный

а вы пробовали Раст?

Я бы хотел таких комментаторов, которые внятно доносят свою мысль разбивая текст на абзацы и предложения.

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

Наличие эпох не заставляет их использовать.

Ну а почему бы и нет.

Разновидность рефакторинга.

Лишь бы свободу выбора оставляли.

В мире .NET-а в них сходили как минимум дважды (client profile, netstandard), и, к счастью, закопали.

Не согласен с предложением "давайте пусть комитетчики бегут не впереди, а позади паровоза".

Потому что именно это они делали с 2003 по 2011 год. Дожидались, когда у зоопарка крутых компиляторов и у мега-библиотек наподобие буста наберётся достаточной статистики и положительного опыта.

А в это время рядовые программисты либо кушали блины с лопаты, либо участвовали в кембрийском взрыве "как бы ещё извратиться и хакнуть те возможности, которые есть сейчас из коробки на копролитических промышленных компиляторах".

Это было весёлое время, но возвращаться в него я категорически не хочу!

Комитетчики продолжат работать над пропозалами и принимать или не принимать их. Но с промежуточной стадией – принятый пропозал не сразу попадает в стандарт, а только после того, как появится минимум две его реализации.

При такой схеме сраные модули попали бы не в C++20 (где их нет до сих пор в нормальном виде), а в C++26 или даже C++29. Чтобы эти гребанные модули, если уж кому-то они так вперлись, смогли бы использовать все желающие. А не только те, кто пишет один проект только на одном clang-е или VC++.

Т.е. условный no_unique_address все так же был бы одобрен в 2020-ом или 2019-ом, но в формальный стандарт был бы включен в 2023-ем или 2026-ом.

принятый пропозал не сразу попадает в стандарт, а только после того, как появится минимум две его реализации

Стандарт это требования к компиляторам. Вы хотите наплодить сущностей и поставить телегу впереди лошади, чтобы сначала была реализация, а потом её стандартизация. Разве это адекватно?

Было: пропозал, обсуждение его, включение в стандарт, реализация компиляторами.

Станет: пропозал, обсуждение, его заморозка, реализация компиляторами, включение в стандарт.

После принятия такой чепухи слово "стандарт" потеряет всякий смысл, потому что компиляторы фичи будут реализовывать по замороженным пропозалам, а "стандарт" останется где-то в стороне, так как он всегда будет отставать от имеющихся де-факто возможностей. Если пропозалы заморожены, то де-факто они станут являться стандартом.

В вашей идее нет ни грамма смысла, это хотелка, которая внесёт кучу неразберихи, не достигнув никакой цели.

Стандарт это требования к компиляторам.

Т.е. вы хотите сказать, что стандарт нужен только компиляторам, а мне, как разработчику на С++, стандарт не нужен. Правильно я вас понял?

В вашей идее нет ни грамма смысла, это хотелка, которая внесёт кучу неразберихи, не достигнув никакой цели.

Можете узбагоиться, про мою хотелку в комитете даже и не узнают.

Да, согласен, эпохи были бы неплохой идеей. Да и в принципе если сейчас не заняться плюсами, то они рано или поздно уйдут в наследие по типу COBOL

Мне это очень напоминает утечку памяти - все больше от С++ вытекают другие языки призванные его "решить" или даже "убить" - тот же Rust, TrapC, Zig, Carbon и другие. И именно из-за такого большого наплавления других языков начинается путаница - вроде все от С++, а каждый не хочет дружить с каждым (ну или я плохо знаю о них,подправьте если что)

И про отдельную стадию с реализацией - вообще хорошая идея, были случаи когда вводили в стандарт, а потом "упс, а оно не работает то оказывается! ¯\_(ツ)_/¯" (вспоминаем тот же плачевный std::auto_ptr)

Ну а про исключения... Мало что сказать могу, лично я имею дело со встраиваемыми системами и игровыми движками, там исключения по определению выключены

И под конец могу сказать что тут уже гонка быстрее вымрет - язык.... или комитет 💀 (ладно, это была плохая шутка)

P.S. Подумал еще об стандартах - на самом деле палка с двумя концами, которую никто не хочет брать. Вот сколько не замечал - все крупные проекты сидят на одних стандартах, никто не хочет обновляться. А если делают - то только в совсем безумных обновлениях, когда прям уверены что могут. И в тоже время комитет С++ "нужно сделать фичу так, чтобы она запустилась из кода С++98" - ну классно просто. И программисты не идут в будущее, и комитет не идет в будущее. Пути эволюции свернули куда-то не туда...

были случаи когда вводили в стандарт, а потом “упс, а оно не работает то оказывается! ¯_(ツ)_/¯” (вспоминаем тот же плачевный std::auto_ptr)

auto_ptr хотя бы в стандарт попал. И им хотя бы было можно пользоваться. В рамках существовавших тогда возможностей.

А вот export template, например, так до пользователей и не дошел. Как и удаленная в итоге из стандарта поддержка GC.

Или, например, std::aligned_storage, который в стандарт сперва включили, а потом исключили. Или std::is_trivial.

Я думаю, что у с++ очень амбициозные цели. Он хочет быть максимально производительным, максимально универсальным, обратно совместимым и при этом желательно понятным.

Но если с++ не загнётся, а преодолеет эти трудности, возможно у нас будет язык, близкий к идеальному. Так скажем граалем всего программисткого комьюнити.

Я думаю, что он не загнется (ведь ассемблеры существуют до сих пор?). Просто в будущем он займет примерно такую же роль, как сейчас LLVM для генерации машинного кода. Супер универсальный высокоуровневый язык программирования без накладных расходов, но писать на нем вручную можно будет только либо отдельные небольшие фрагменты, либо полностью автоматически генерированный код.

С++ стандартов где-то позже 11 или 17 существенно переусложнён и идёт по пути языка Ада - т.е. нормальный программист может держать в голове и постоянно пользоваться ограниченным объемом возможностей языка.

Но каждый использует при этом разный набор возможностей.

В результате, взглянув на код другого программиста, возникает проблема с пониманием. И это - очень, очень большая проблема.

Язык должен быть относительно прост, так чтобы в голову очень среднего программиста помещался целиком.

А если в вашей конкретной предметной области возникают какие-то особо странные и удивительные вещи, требующие удивительной гибкости - значит, пора встраивать в вашу систему интерпретируемый язык. Классика же: "в любой по-настоящему сложной программной системе возникает кривая самопальная реализация языка LISP".

--------------

По поводу исключений и обработки ошибок:

  1. Исключения имеют практический смысл в двух случаях: а) в модульных тестах для выхода по панике без падения процесса б) для использования вместо longjump.

  2. Если у вас в коде есть ошибка, которую вы не знаете как обработать - кидать исключение - самая глупая идея, которая только может придти вам в голову. Самое разумное в этом случае: немедленно завершить процесс. Попытка продолжения скорее всего приведёт к худшим последствиям, чем немедленное завершение - например, будет не просто потеря, но искажение данных. Если в какой-то ситуации вы провели анализ и выяснили, что "в данном месте, деление на ноль может случиться при вот таких условия, и в этом случае мы должны сделать вот это" - вам не надо кидать исключение. А если вы не можете понять, "как вот здесь оказалось деление на ноль" - немедленно паникуйте с дампом памяти.

А если в вашей конкретной предметной области возникают какие-то особо странные и удивительные вещи, требующие удивительной гибкости - значит, пора встраивать в вашу систему интерпретируемый язык.

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

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

Самое разумное в этом случае: немедленно завершить процесс.

Поражаюсь подобной категоричности. Возможно, вы пока еще не встречались с ситуациями, когда ронять весь процесс не вариант, а исключение, возникшее где-то в обработке одного запроса, можно перехватить и проигнорировать проблемный запрос.

Промахнулся кнопкой +1 :(

Так и есть. Не любая ошибка это катастрофа.

Скажем пришел неправильный запрос от клиента это ведь не катастрофа.

Скажем пришел неправильный запрос от клиента это ведь не катастрофа.

Особенно когда этих запросов еще 100500 в очереди, а рестарт приложения занимает часы (буквально, не фигура речи).

и идёт по пути языка Ада …

C++ в принципе не может идти по пути Ада, потому что для Ада сперва создавались согласованные требования и только после этого разрабатывался язык и его синтаксис, тогда как в С++ все с точностью наоборот. Сперва сделали С с классами для реализации ограниченного набора идей, а уже после начали на него наворачивать новые концепции, пытаясь их встроить в уже существующий синтаксис.

Если у вас в коде есть ошибка, которую вы не знаете как обработать - кидать исключение - самая глупая идея, которая только может придти вам в голову. Самое разумное в этом случае: немедленно завершить процесс …

Как по мне, так самое глупое, что может сделать программист отдельного компонента (функции, метода или библиотеки), это завершать работу всего приложение, если не знает как обработать в данный момент ошибочную ситуацию.

Использование исключений обязано определяться требованиями, а никак не личными предпочтениями разработчика или его пониманием (или не пониманием) обработки данных.

Использование исключений обязано определяться требованиями, а никак не личными предпочтениями разработчика или его пониманием (или не пониманием) обработки данных.

{исключений/memset/memmove/slice/append/pop/{ptrinit/ptrdeinit/namespacebranching}}

и при компиляции я должен видеть ход генерация кода в какую ветку и почему он пошел

тоесть синтаксис должен упроститься и стать по единой спецификации простым инструментом по стандарту какому-то, потомучто есть уже набор базовых критериев(пк базовые критерии, процессоры базовые критерии, вся хурма с железками, при добавлении новой железки должна попадать в базовый критерий тут я не уверен, но мне кажется у ЭВМ есть базовые критерии, в т.ч. и ОС базовые критерии -> это прикодит к критериям языков, как они общаются с системой), для обеспечения цикла программы, прошло 100 лет образно, я не верю, что какие-то моменты тупо в воздухе летают до сих пор

образно набор опций/правил - на основе реальной железки получается потомучто они все работают на каких-то принципах, а значит соблюдаются какие-то критерии - критериев для исключений
правила когда вставка в слайс, а когда в указатель, а когда в мемсет, а когда, в меммув
...
итак далее, и эти критерии от сегодняшнего масштаба ЭВМ частично уже базовые я считаю, просто должны быть правила прозрачности критериев, как критерий проектирования ПК с ОС образно

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

А если, например, функции передали nullptr вместо имени файла, и в документации этой функции не описано поведение в такой ситуации - я немедленно паникую. Или если передали структуру, в которой должны быть инварианты, а они не выполняются - паникую.

Надо чётко понимать, когда ошибка - от внешних данных, и когда - от внутренних. Никогда нельзя давать возможность скрыть ошибку во внутренних данных или структуре - это приведёт к тому, что а) дальнейшее поведение программы будет непредсказуемым б) обнаружить, с чего всё началось - скорее всего не получится.

Называется это "защитное программирование", и нет никогда никаких причин его не использовать.

Лично я пришёл к этому не сразу, на заре карьеры у меня было стремление сделать свои программы "выживальщиками", в которых может продолжаться функционирование, даже если "что-то идёт не так". Со временем я на личном опыте понял, что последствия от "продолжения функционирования при ошибках" куда существеннее, чем немедленная паника.

Исключение, которое в принципе можно обработать - это не исключение, а код ошибки. …

Можно сказать, что код ошибки, это информация для вызывающей стороны, причем в рамках контракта (API) для вызываемой функции. И будет вызывающая сторона обрабатывать код ошибки или нет, это её дело. Тогда как исключение, это возникновение ситуации выходящей за рамки согласованного контракта вызываемой функции (например, не предусмотрен код ошибки, как в вашем примере с нулевым указателем).

Но по большому счету это не важно. Исключение, это только инструмент, и всегда можно компилировать с -fno-exceptions

И будет вызывающая сторона обрабатывать код ошибки или нет, это её дело.

Как и в случае с исключениями.

Тогда как исключение, это возникновение ситуации выходящей за рамки согласованного контракта вызываемой функции

И как же быть с исключениями, которые являются частью контракта?

и всегда можно компилировать с -fno-exceptions

Вот так прямо и всегда?

Разница в том, что код ошибки проигнорировать можно, а исключение - нельзя.

Вернее, можно проигнорировать и то и другое локально, но исключение полезет вверх по стеку и его придётся обработать выше. Код возврата проигнорировать легче (промолчим про лист подорожника по имени [[nodiscard]]).

Далее, если произошла ошибочная ситуация, то не факт что её можно корректно парировать ровно в месте непосредственного вызова. Нередко корректная возможна лишь на несколько фреймов выше. И это прекрасно понимают в ООП-языках, но почему-то борются с этим в С++...

Лично я пришёл к этому не сразу, на заре карьеры у меня было стремление сделать свои программы "выживальщиками", в которых может продолжаться функционирование, даже если "что-то идёт не так". Со временем я на личном опыте понял, что последствия от "продолжения функционирования при ошибках" куда существеннее, чем немедленная паника.

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

Т.е. речь ни в коем случае не идет о продолжении работы любой ценой, но и не о том, чтобы бросить все на полпути и рухнуть с треском.

Освобождение памяти, закрытие файлов и откат транзакций - это всё обеспечивает операционная система. Пытаться это сделать, когда у вас в коде непредусмотренное состояние в которое непонятно как попали и непонятно что делать - верный путь к тому, чтобы сделать значительно хуже.

Ну вот представьте себе, у вас - редактор для файла сложного формата. Пользователь наредактировал всякого, данные у вас в памяти и тут возникла ошибка. Если вы просто "упадёте" - файл останется без этого наредактированного, но хоть корректный. А вот если попытаетесь сохраниться...

Пытаться это сделать, когда у вас в коде непредусмотренное состояние в которое непонятно как попали и непонятно что делать - верный путь к тому, чтобы сделать значительно хуже.

Непонятно каким образом это относится к исключением и почему на других подходах этого не будет.

Ну вот представьте себе, у вас - редактор для файла сложного формата. Пользователь наредактировал всякого, данные у вас в памяти и тут возникла ошибка.

Хороший пример, кстати говоря. Вот у вас редактор, вы в нем корпели несколько часов, затем параллельно запустили какую-то тяжелую программулину и выбрали всю имеющуюся оперативную память. Захотели в редакторе сделать какое-то действие, поймали bad_alloc. И что, вы ратуете за то, что редактор должен просто упасть?

Если вы просто “упадёте” - файл останется без этого наредактированного, но хоть корректный.

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

Освобождение памяти, закрытие файлов и откат транзакций - это всё обеспечивает операционная система.

Ой ли? А утечки памяти откуда берутся? А сообщение "файл невозможно удалить т.к. он занят другим процессом" после падения этого самого процесса откуда берется?

Я не говорю о более сложных ситуациях когда изменили данные в 5-ти таблицах, на изменении в 6-й упали с треском и в результате получили неконсистентность данных.

Если вы просто "упадёте" - файл останется без этого наредактированного, но хоть корректный. А вот если попытаетесь сохраниться...

А это тут при чем? Речь о том, что вы должны восстановить "как было до запуска вашей программы". Откатить все изменения (если они не доведены до конца), освободить все файлы, освободить всю выделенную динамически память.

Ой ли? А утечки памяти откуда берутся? А сообщение “файл невозможно удалить т.к. он занят другим процессом” после падения этого самого процесса откуда берется?

Справедливости ради: а какие ОС общего назначения не подчищают память и не закрывают автоматически открытые процессом файлы?

Что до невозможности удалить файл, который занят другим процессом, то такое запросто возможно, когда приложение использует многопроцессную архитектуру: родитель запускает один или несколько дочерних процессов. Какой-то из них падает (например, родитель), а остальные не могут по каким-то причинам диагностировать исчезновение родителя и продолжают держать открытыми ресурсы.

Еще весело бывает, когда два процесса общаются друг с другом через shared-memory и один из них внезапно падает. Что именно остается при этом в shared-memory – никому не ведомо.

Справедливости ради: а какие ОС общего назначения не подчищают память и не закрывают автоматически открытые процессом файлы?

Честно скажу - не знаю. Но проблема утечек памяти-то есть. Т.е. если ваш процесс динамически выделил память а потом ее не освободил на выходе, возникает утечка.

Какой-то из них падает (например, родитель), а остальные не могут по каким-то причинам диагностировать исчезновение родителя и продолжают держать открытыми ресурсы.

И это тоже.

Еще весело бывает, когда два процесса общаются друг с другом через shared-memory и один из них внезапно падает. 

И это.

Про транзакции уже писал. Любая транзакция должна быть или закоммичена, или откачена.

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

Честно скажу - не знаю.

Тогда разговор вряд ли будет предметным.

Но проблема утечек памяти-то есть.

Где?

Т.е. если ваш процесс динамически выделил память а потом ее не освободил на выходе, возникает утечка.

Не доводилось наблюдать такого в Windows, Linux и FreeBSD.

Проблемы с тем, что какой-то файл оказался заблокированным в Windows после того, как работавший с ним процесс упал, случаются. Но здесь я не в курсе того, кто виновник – Windows, который не чистит ресурсы, или же сам процесс, который запустил дочерный процесс и на самом деле файл блокируется дочерним процессом.

Блокировки файлов были свойственны сетевым дискам.

В никсах блокировками предпочитали не пользоваться. Не вникал в причины, скорее всего исторические

Блокировки файлов были свойственны сетевым дискам.

При работе с USB-флешками в Windows подобное наблюдается регулярно. Посмотришь на содержимое флешки в FAR-е, затем закроешь FAR, попытаешься безопасно извлечь накопитель, а Windows говорит, что он еще каким-то приложением используется.

Правда, качество работы Windows c каждым годом все ниже и ниже…

В винде утечки памяти наблюдал неоднократно. Именно той, которая была выделена динамически. А с учетом того, что в С++ динамическая память направо и налево выделяется...

GC таки не зря придумали...

В винде утечки памяти наблюдал неоднократно.

Со времен перехода на Windows NT и ее наследников еще ни разу не видел, чтобы при закрытии проблемного приложения память системе не отдавалась.

Я это на семерке наблюдал. И на 2000 (которая NT5)

Ну вот а я не наблюдал начиная с Windows NT 3.51. И если после снятого процесса в Windows не возвращалась память, то значит ситуация там была гораздо более сложная, чем кажется на первый взгляд. Может эта память разделялась с каким-то другим процессом или же процесс на самом деле оставался “висеть” и не был полностью “убит” операционкой.

Я наблюдал это именно на динамически выделенной памяти. Которая выделяется "где-то там" без привязки к конкретному процессу.

В винде (да и в линуксе тоже) процессы не изолированы, как например, в той же AC/400 под которую пишу последние 8 лет - тут нет процессов, есть задания (job) каждое из которых является изолированным контейнером (а внутри задания еще есть группы активации - подконтейнеры). Но тут могут быть свои приколы.

Которая выделяется “где-то там” без привязки к конкретному процессу.

А это как вообще?

В винде (да и в линуксе тоже) процессы не изолированы

Тут остается только воскликнуть “Да что вы говорите?!!!”

Тут остается только воскликнуть “Да что вы говорите?!!!”

Вот то и говорю... Какая-то изоляция там есть, конечно. Но не 100% как для заданий в той же АСке где задание - контейнер. Не зря же в винде/линухе всякие докеры-шмокеры придумывают... Как раз по причине недостаточной изоляции процессов.

Вот то и говорю…

Выглядит как свидетельство очевидца – где-то что-то когда-то как-то, но где что и как точно не помню.

Какая-то изоляция там есть, конечно.

Там как раз достаточная изоляция для контекста разговора, а именно “Освобождение памяти, закрытие файлов и откат транзакций - это всё обеспечивает операционная система.” Освобождение памяти и закрытие файлов после завершения процесса – это как раз непосредственная задача ОС общего назначения. И Windows/Linux/FreeBSD из того, что я видел, с этой задачей прекрасно справляются.

Не зря же в винде/линухе всякие докеры-шмокеры придумывают… Как раз по причине недостаточной изоляции процессов.

Ну-ну, ну-ну.

Выглядит как свидетельство очевидца – где-то что-то когда-то как-то, но где что и как точно не помню.

Помилуйте, я под винду не писал уже почти 9 лет... И слава богу :-)

Но проблем там хватало. Особенно если поток не один, а продукт сложнее (а мне приходилось заниматься разработкой достаточно сложных вещей, как правило, работающих в фоне 24/7) чем небольшая утилитка командной строки.

И санитайзерами приходилось пользоваться для выявления неосвобожденных объектов, и с потоками извертываться чтобы один при падении не тащил все остальное...

Отсюда и твердое убеждение что обработка ошибок, особенно критических, не такое простое дело и надеяться что система все сделает за вас очень самонадеянно.

Да и "транзакция" может быть сложнее чем обычная транзакция БД. И вообще это может быть не БД совсем. Точнее, не только БД, но что-то более распределенное (получил из одного канала, никоем образом обработал - отправил в другой канал и все это с подтверждениями что получил и что отправленное дошло), но тоже требующее транзакционного подхода.

Особенно если поток не один, а продукт сложнее (а мне приходилось заниматься разработкой достаточно сложных вещей, как правило, работающих в фоне 24/7) чем небольшая утилитка командной строки.

Вы всерьез думаете, что разговариваете здесь с людьми, которые сложнее примитивных утилит командной строки ничего не делали?

И санитайзерами приходилось пользоваться для выявления неосвобожденных объектов, и с потоками извертываться чтобы один при падении не тащил все остальное…

Многопоточное программирование и изоляция процессов в ОС – это сильно разные вещи. Если мы говорим про ОС общего назначения, то внутри процесса его треды разделяют общую память и один процесс не подчистил за собой, то это утечка памяти в процессе.

Но если процесс выделил память (не важно какой из тредов процесса) или взял у ОС какой-то ресурс (открыл файл, пайп или сокет), то ОС общего назначения обязана все это подчистить за процессом после того, как процесс завершился.

Если этого не происходит, то либо в ОС где-то есть глюк, либо ситуация на самом деле не так проста.

Да и “транзакция” может быть сложнее чем обычная транзакция БД.

Я вообще не знаю что именно тов.@aeder подразумевал под “транзацией”. Но вот то, что он сказал про закрытие файлов и освобождение памяти именно таким образом и происходит в ОС общего назначения.

Вы всерьез думаете, что разговариваете здесь с людьми, которые сложнее примитивных утилит командной строки ничего не делали?

Я надеюсь что это не так. И при этом странно читать утверждения о том, что программу можно просто уронить , а система сама все подчистит.

Я вообще не знаю что именно тов.@aeder подразумевал под “транзацией”.

Транзакция в общем случае - последовательность действий, которая должна быть завершена до конца. А в случае ошибки должен быть откат с исходному состоянию.

Я занимался разработкой ПО для системы мониторинга инженерного оборудования зданий. Там с одной стороны сеть промконтроллеров с подключенными датчиками и механизмами, в другой - набор интерфейсных клиентов. Между ними микроядро (которое, собственно, я и делал) - оно занималось согласование форматов и маршрутизацией сигналов.

Так вот там транзакция начиналась на момент формирование сигнала на контроллере и заканчивалась отображением информации в интерфейсном клиенте. или наоборот - от формирования команды в клиенте и до исполнения ее контроллером.

Потерять бесследно сигнал или команду по дороге - непозволительно. Отправляющая сторона всегда должна четко знать - дошла посылка до адресата или нет (нужна перепосылка или нет).

Второй пример транзакции - проведение платежа в банках. Там нельзя окончательно списать деньги со счета пока не будет получено подтверждение от получателя. А это может занять от нескольких минут до нескольких дней. И при этом платежей может быть несколько разным получателя и нельзя допустить технического овердрафта. Там свои механизмы, более сложные, нежели просто транзакция БД.

Изменение нескольких связанных по данным файлов (не таблиц БД) - тоже транзакция. Если упали в середине и не откатили то, что уже поменяли - нарушите связность данных.

Примеров мильен можно привести. И везде придете к тому, что просто так грохнуть процесс без корректной обработки аварийного завершения чревато.

И при этом странно читать утверждения о том, что программу можно просто уронить , а система сама все подчистит.

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

Спорить с таким предположением не озвучивая условий когда это не так (например, специализированные ОС реального времени, где может не быть изоляции процессов), мягко говоря, не конструктивно.

Да. При условии что приложение не работает со связанными в нескольких файла данными, не обеспечивает работу в транзакционном режиме и еще много чего не делает.

Т.е. это что-то с достаточно простой логикой (относительно внешних данных хотя бы) и достаточно изолированное (не являющееся частью более сложной системы).

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

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

Это не имеет отношение к разговору о том, что чистит за приложением ОС в случае завершения приложения.

Все эти ваши усложнения, типа “связанная в нескольких файла информация” и пр. дребень – это увод разговора совсем в другую сторону. Вы не можете надеятся на то, что ваше приложение в 100% случаев гарантировано получит возможность корректно все это закрыть, завершить/откатить транзакции, послать какие-то комманды на удаленный сервер или подключенному к компьютеру контроллеру и т.д., и т.п.

Не может просто потому, что ваш процесс могут кильнуть из TaskManager или top/htop или вообще каким-то другим способом (например, это может сделать ООМ-killer в Linux-е если выберет ваш процесс жертвой).

Поэтому обеспечение надежности при подобных сбоях – это отдельная прикладная задача, не имеющая прямого отношения к исключениям в коде. И к тому, за что отвечает ОС общего назначения.

Поэтому хватит флудить.

А что вы делали, когда кто-то выключал питание на чём-то в промежутке? На свитче или датчике, например?

Про проведение платежа в банке - погуглите, что такое транзакция в SQL и как она реализуется технически. Там всё сделано именно так, чтобы даже выключение питание на всех серверах разом не оставила систему в неопределённом состоянии. И полагаться при этом на то, что "программа подчистит за собой" никто и никогда не будет, потому что завершение программы путём аварийного выключения питания - это наиболее вероятный способ завершения работы для программ, которые рассчитаны на работу 24/7.

Если Вы делаете открытую систему, пригодную для расширения, то скорее всего коды ошибок создадут больше проблем, чем решат.

Было бы здорово сделать новый edition, который бы позволял избавится от необходимости иметь header-файлы и позволил бы обращаться к любым декларациям в файле, вне зависимости от очереди определения

Ибо формат модулей уж очень удручает…

Э.. как бы на этом построен весь C++.
Порядок имеет значение.
Добавляем перегрузку и получаем другие инварианты.

#include <string>
#include <type_traits>

// First overload
int f(int a) { return a; }

template<typename T>
concept can_call_f = requires {
    f(T{});
};

void before() {
    static_assert( can_call_f<int>);
    static_assert(!can_call_f<std::string>);

    f(1);
}

// New overload
std::string f(std::string c) { return c; }

template<typename T>
concept can_call_f_2 = requires {
    f(T{});
};

void after() {
    static_assert(can_call_f_2<int>);
    static_assert(can_call_f_2<std::string>);

    f("a");
}

int main()
{
}

Вы написали всего пару улучшений, а получили бугурт на 160 коментов 😁 Этого мало, нужно более десяти улучшений c++. Если вы их опишите, новая статья будет не менее обсуждаемой

Мне кажется, что если бы я предложил выбросить из стандарта модули, которые каким-то образом впихнули в C++20, а тех людей, благодаря которым модули внедрили в стандарт, приговорить к 10 годам растрела, то можно было бы собрать еще 200 комментариев.

Они заслуживают расстрела уже только за то, что отменили нетворкинг и объяснив это тем, что не смогли красиво его реализовать на модулях, которые сами же и придумали

Здесь наши пути расходятся, т.к. я придерживаюсь мнения, что всякие networking-и, как и XML- или JSON-парсеры, не должны быть в стандарте вообще. Подобное должно подключаться к проекту посредством штатного менеджера зависимостей. Но комитет пока такого не родил, а если и родит, то это будет такой же ужас, как и всратые модули из C++20, а то и гораздо хуже.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации