
Комментарии 72
то, что есть в тех компиляторах, которыми приходится пользоваться. Собственно, это и есть реальный С++. …
На самом деле скорее всего есть четыре, а не три типа С++. Четвертый вид С++идет идет следующим и называется “то, что реально пользуются в продакшене”. Обычно это возможности, которые отстают от третьего пункта на несколько поколений стандарта. Возможности, у кого известны все подводные камни и нюансы и есть наработана легаси практика.
А, нет. Есть еще и пяты тип С++ - это весь объем легаси кода, который тянет за собой обязательное требование по обратной совместимости со самыми старыми версиями и именно этоот пятый вид и тормозит развитие С++.
Типа в начале .cpp-файла указывается версия С++, для которой в этом файле пишется код. Но, к сожалению, эту идею в комитете похоронили.
Идея привязки кода к разным версиям стандарта существует до сих пор и частично прорабатывается в профилях безопасности. Просто это привязка реализуется не к нумерации стандарта С++, а к его возможностям, которые не связанных одной версией, что гораздо более правильно, как мне кажется.
Перед тем, как добавлять эпохи, надо быть уверенным, что для новых проектов вообще будут выбирать современный С++ хоть сколько нибудь часто, в чем я оооочень сомневаюсь.
Даже если на C++ в ближайшие 40 лет будут только сопровождать существующие проекты, то все равно выигрыш от эпох будет, т.к. сопровождение – это не только баг-фиксинг в уже написанном коде, но и дописывание новых фрагментов. И новые фрагменты лучше дописывать на более современном диалекте C++.
Новые фрагменты надо писать в стиле проекта, чтобы он легко "читался". Если ты в проекте видишь солянку от void* до вариадиков, это зло.
Так что я стою на своем: отдельные библиотеки, покрытые тестами, можно писать как душе угодно. Пока они хорошо работают, их все равно читать никто не будет. А новый С++ не заточен на читателей, он для писателей.
К сожалению, в больших проектах с большим временем жизни постоянно приходится возвращаться к написанному ранее - тут через три года безупречной работе в проде вдруг вылез какой-то баг (совершенно экзотический, никому в голову не приходило что звезды вот так встанут), тут бизнес-логика изменилась, тут с ростом объема данных производительности не хватает и надо что-то придумывать... И вот любой писатель, вынужден сначала быть читателем...
Новые фрагменты надо писать в стиле проекта, чтобы он легко “читался”.
Т.е. если проект стартовал в 1990-е и был написан в стиле “ООП головного мозга”, то в таком же стиле он и должен дописываться в 2030-ом?
Если он хорошо читается и удовлетворяет требованием производительности - почему нет?
Основная проблема в том, что порой чтобы понять что именно делает код из (условно) 100 строк вам надо продраться через 1000 (опять условно) строк другого кода, содержащего десяток слоев абстракций. И не дай бог, если проблема (дефект бизнес-логики, дефект производительности...) окажется где-то там...
Если он хорошо читается и удовлетворяет требованием производительности - почему нет?
Потому что прогресс не стоит на месте. Можно продолжать писать вложенные for-ы на страницу, а можно написать одну-две строчку на ranges.
Если эти 1-2 строчки будут работать быстрее - почему нет?
А если это потянет 5 уровней абстракций и кучу выделений-освобождений памяти в результате которых упадет производительность - точно нет.
И отправить этот код и все, что его хоть как-то касается, тестерам на перетест...
Как будто другие типы изменений не нуждаются в перетестировании…
Нуждаются, но, ведь, разговор идет о перписывание работающего, протестированного, устривающего по скорости кода на "новый С++" ради "прогресс не стоит на месте".
разговор идет о перписывание работающего, протестированного, устривающего по скорости кода на “новый С++” ради “прогресс не стоит на месте”.
Не знаю кто вел разговор об этом, но точно не я.
Я изначально говорил про дописывание новых фрагментов. Типа была софтина, которая интегрировалась с внешними системами 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 лет в разработке много чем занимался в разных областях и много чего повидал.
В том и дело что задания у нас крашатся в самых исключительных случаях.
У вас – да. У других может быть сильно иначе. Поэтому не нужно на все смотреть через призму собственных задач.
Я смотрю сквозь призму опыта. Мне не кажется хорошим стилем просто крашить задачу без подробного объяснения причин в стиле "Что-то пошло не так...".
Для меня обработка ошибок всегда была более сложным местом, чем реализация логики.
std::set_terminate ?
А так и делаем. Структурированная ошибка называется.
Беда в том, что в этом самом 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 все-таки отрицательный. Конструктор бросает исключение. Это исключение где-то выше ловится.
Все же это логика.
Тогда ошибка должна сформировался не на единичный объект, а на блок вычислений. Я бы так спроектировал
Увы, но использование исключений катастрофически отрицательно влияет на производительность... Вплоть до того, что на нагрузочном тестировании вам вынесут приговор - "20% ресурсов процессора уходит на работу с исключениями - переделать".
Исключение допустимо в условиях возникновения критической, блокирующей ошибки. Когда "ой, все..." - дальнейшая работа невозможна. Но не как средство возврата обрабатываемой ошибки, подразумевающий какую-то дальнейшую логику.
Иииии…?
Я не требую использовать исключения в качестве единственного варианта обработки ошибок. Я пишу про то, что в определенных случаях исключения очень удобны, а прерывание выполнения потока команд так или иначе присутствует во всех языках программирования, даже в тех, у которых исключения якобы отсутствуют.
Поэтому лучше иметь стандартизированные исключения с возможностью их обработки в рантайме, чем в каждом случае завершать выполнение приложения по abort или с помощью хаков пытаться реализовывать перехват подобных ошибок.
Я не спорю. Больше скажу - удобнее всего когда исключения не языковые, а системные. С возможностью пробрасывать их на любой уровне стека вызовов (вплоть до за пределы своей программы тому, кто ее вызвал с уверенностью что там оно тоже будет перехвачено и обработано вне зависимости от того, на чем та программа написана).
Но это механизм обработки критических, блокирующих ошибок. Это тяжелый механизм, который может использоваться только в тех случаях, когда терять уже нечего и производительность уже не важна, важно корректно завершение с подробной фиксацией что где и почему.
А вот против того, чтобы использовать исключения в качестве механизма возврата любой ошибки, даже не блокирующей, которую надо просто зафиксировать и идти дальше.
Как пример - обработка 25 000 000 записей в рамках заданного временного окна и условиях ограничения ресурсов. В 10-ти записях (условно, может быть 100, может быть 1000) возникает какая-то ошибка, связанная со специфическим набором данных в конкретной записи (скорее всего, какое-то количество таких ошибок будет всегда). Такие ошибки необходимо фиксировать для последующего ручного разбора. Но все остальные записи надо продолжать обрабатывать. Вот тут исключения категорически непригодны т.к. снижают производительность.
… вот против того, чтобы использовать исключения в качестве механизма возврата любой ошибки, даже не блокирующей, которую надо просто зафиксировать и идти дальше.
Так именно это я и имею ввиду. Исключения нельзя использовать для обработки часто возникающих ситуаций. Их назначение - прерывать поток выполнения при возникновении не восстановимой ситуации, когда становится не важна производительность и требуется корректно обработать завершение приложения. Но если на более высоком уровне есть механизм обработки подобных ситуаций, то пусть он перехватывает исключения и продолжает работу приложения дальше (например вот так Избавляемся от ошибок Segmentation fault из-за переполнения стека в С++).
Другими словами, исключение, это не обработчик ошибок, а возникновение события достаточного для завершения работы всего приложения, но с возможностью его обработки на более высоких уровнях.
Другими словами, исключение, это не обработчик ошибок, а возникновение события достаточного для завершения работы всего приложения
Выделено в корне неправильно.
Зачем развивать то, что должно быть разделено на несколько различных языков? Да, у таких языков будет похожий синтаксис, но задачи будут решаться различные. Например, "С++ с указателями", "С++ без указателей"; "С++ с модулями", "С++ без модулей"; ... Для программирования пользовательского интерфейса нужно что-то своё, для работы с БД — что-то другое. Зачем всё грести в один монструозный язык, который и за всю жизнь не изучишь?
И каждого варианта свои стандарты, свои компиляторы, возможно свои особенности синтаксиса … И все это должно иметь возможность взаимодействия между сбой. Как представлю, бррр… :-(
С, C++, ObjC, С#, C^^, D. Годится? =)
Комитет должен отойти от практики публикации опережающих реальность спецификаций. Вместо того, чтобы декларировать свои фантазии …
Мне кажется, что в С++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++ помогли бы
editions и отрезание deprecated. Хотя не уверен, удастся ли отрезать неявные преобразования типов - врождённый дефект языка для совместимости с Си. Уронили его в детстве с лестницы, последствия удара головой.
Нормальный менеджер пакетов вроде 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 компилятор пише что он там собирает, диагностические сообщения какие проблемы если не собралось.
Некоторые под выхлопом компилятора понимают не сообщения об ошибках или предупреждениях, а генерируемый компилятором ассемблерный код.
Честно говоря ошибки этого класса достаточно редки.
Я бы тогда уж смотрел на артефакты плавающих вычислений
Честно говоря ошибки этого класса достаточно редки.
Хуже всего то, что о таких неявных преобразованиях многие даже не знают. А из тех, кто знает, многие не помнят.
а я бы хотел язык такой стандарт языка С++(я понимаю что это почти невозможно), который схож с растом, суть в том, что в моём представляемом не существующем языке, конструкции языка типо нумерик/концепт/...всё что режет глаз элегантно как-то убрано в кишки языка и пускай язык сам решает в момент компиляции где там что, потомучто нагромождения кишок где что создаётся иногда выглядит как битва за то чтобы писать чисто, и мешает этому не только легаси, но и новшества которые надо в коде буквально как маркер описывать, и вот код уже около нечитаемый(важный нюанс чтобы были правила предсказуемости выведения это не копия раста, но типо синтаксис С++ его все фишки, но они в кишках выводятся сами правилами какими-то)
чтобы std:: сам понимал своими правилами - да пусть эти правила будут жестокими, но чтобы я не писал уже std, чтобы модули из коробки, удобное многопоточие, все что бы было убрано в кишки и выводилось какими-то правилами касательно шумовых-маркерных конструкций так и вообще всего почти языка, чтобы писать только тип портировать что надо и оно там выводилось, а компилятор в компил тайме показывал ветку узла куда пошла генерация - тоже по каким-то правилам
текущий С++23/26 мне нравится, но я уже присматриваюсь всё чаще на Раст, он как-то более читабельный
а вы пробовали Раст?
Не знаю, те же эпохи - это по-моему наоборот путь в никуда. То есть я не могу в существующем модуле просто взять и применить новые фичи "по месту" точечно, я должен сначала переписать те места, которые данная эпоха "не поддерживает", ну такое себе упражнение. Если упороться в такое, то проще уж сразу писать новые модули на каком-нибудь карбоне, но желающих это делать в массовом масштабе очевидно нет.
Что могло бы помочь в дальнейшем развитии C++, но вряд ли произойдет