Pull to refresh

Comments 555

А как в сравнении с Haskell?

Отлично, на самом деле. В Haskell что ни говори, тяжело писать большие приложения. Rust берёт же от хаскеля много хорошего, и при этом даёт уверенность, что у тебя не утекают от лени thunk-и, что скомпилированный код скорее всего отлично работает, что твои попытки оптимизации работают (привет, мемоизация и прочая хаскелевская муть). Rust - это быстрый Haskell с императивностью и мутабельностью в нужных местах и по делу.

Ну нет, в Расте нет типов высшего порядка

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

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

расскажите подробнее или скиньте что-нибудь почитать о них в хаскель, пожалуйста

нашел только о функциях высшего порядка

Типом высшего порядка является даже обычный дженерик. Или даже ссылка (не ссылка на конкретный тип, а ссылка вообще),

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

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

К слову, в плюсах типы высшего порядка выражаются концептами и не SFINAE, а банальными typedef в шаблонах.

Я под типами высшего порядка понимаю "классы типов".

Т.е. концепт в плюсах описывает какой-то класс типов, которые ему удовлетворяют. Аналогично с трейтами в раст.

Вы наверно имеете ввиду, что ТВП, по аналогии с ФВП, являются данными?

У типов высшего порядка есть определение, ТВП - это тип рода * -> * и выше. Иными словами, это функция, аргументом и значением которой являются типы.

Например, std::vector - это ТВП, он сопоставляет любому типу-параметру T тип-значение std::vector<T>. Обратным к нему будет ТВП, который сопоставляет любому типу-параметру T тип-значение T::value_type.

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

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

это да. я не настоящий функциональщик, но иногда посматриваю в ту сторону. Вот на страничке Control.Lens.Operators перечисляются эти операторы:

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

Hidden text

(#%%=)
(#%%~)
(#%=)
(#%~)
(#)
(#=)
(#~)
(%%=)
(%%@=)
(%%@~)
(%%~)
(%=)
(%@=)
(%@~)
(%~)
(&&=)
(&&~)
(&)
(&~)
(=) (~)
(=) (~)
(+=)
(+~)
(-=)
(-~)
(...)
(.=)
(.>)
(.@=)
(.@~)
(.~)
(//=)
(//~)
(<#%=)
(<#%~)
(<#=)
(<#~)
(<%=)
(<%@=)
(<%@~)
(<%~)
(<&&=)
(<&&~)
(<&>)
(<=) (<~)
(<=) (<~)
(<+=)
(<+~)
(<-=)
(<-~)
(<.)
(<.=)
(<.>)
(<.~)
(<//=)
(<//~)
(<<%=)
(<<%@=)
(<<%@~)
(<<%~)
(<<&&=)
(<<&&~)
(<<=) (<<~)
(<<=) (<<~)
(<<+=)
(<<+~)
(<<-=)
(<<-~)
(<<.=)
(<<.~)
(<<//=)
(<<//~)
(<<<>=)
(<<<>~)
(<<>=)
(<<>~)
(<<?=)
(<<?~)
(<<^=)
(<<^^=)
(<<^^~)
(<<^~)
(<<||=)
(<<||~)
(<<~)
(<>)
(<>=)
(<>~)
(<?=)
(<?~)
(<^=)
(<^^=)
(<^^~)
(<^~)
(<|)
(<||=)
(<||~)
(<~)
(?=)
(??)
(?~)
(^#)
(^.)
(^..)
(^=)
(^?!)
(^?)
(^@.)
(^@..)
(^@?!)
(^@?)
(^^=)
(^^~)
(^~)
(|>)
(||=)
(||~)

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

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

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

И вот пример могучей силы оптики из этой книги:

-- Flip the 2nd bit of each number to a 0
>>> [1, 2, 3, 4] & traversed . bitAt 1 %~ not
[3,0,1,6]

Круто конечно, как ловко и кратко но какой ценой! Это же надо реально изучить, врубиться и постоянно освежать эти знания.

А чтобы удобно работать с многократно вложенными друг в друга структурами данных, вместо линз лучше использовать record dot preprocessor.

Это вы вывели массив. а код выше его модифицирует. Должно быть вот так:

for (int i=0; i<array.Length; i++)
    array[i] ^= 0x02;

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

По-моему, это ghci - REPL для хаскеля.

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

В планах на изучить Haskell есть (может еще F#), но скорее для того, чтобы посмотреть ~~есть ли жизнь на марсе~~ как живется в полностью функциональном языке без доступа к мутабельности, может быть позаимствовать какие-нибудь подходы.

в F# ТВП и тру монад нету если что

В Haskell есть мутабельность

И там и там 2 вакансии, так что равны +-

Кстати нет. Не так давно видел вакансии на Rust в Сбере, МТС и Тиньке помимо мелких компаний и стартапов. Ну и зарубежных удаленных еще больше. И поскольку язык молодой требований из серии 5-10+ лет опыта на нем нет.

На Rust вакансий все же раз в 20 больше, чем на Haskell. С другой стороны количество Rust-истов гораздо больше Haskell-истов.

В моем регионе последние пару лет что я слежу все, я повторю ВСЕ из примерно 2 десятков открытых вакансий в любой момент времени это крипта. 3 года назад мне повезло увидеть 1 вакансию на расте и не крипта.

UFO just landed and posted this here

UB это на самом деле так страшно или, к примеру, в Arduino с ним никогда и не столкнёшься?

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

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

Примеров, конечно же никто приводить не стал..

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

Можете привести какой-нибудь пример? Потому, что пока не очень понятно о чем вы.

Не "могут", а "должны быть".

UFO just landed and posted this here

UB - это не обязательно только про обращение к неинициализированному или некорректному участку памяти. UB - это, например, переполнение знаковой интовой переменной. Да, вы можете предположить, что есть у вас signed integer и вы прибавляете к нему +1, то компилятор просто сгенерирует процессорную инструкцию инкремента или сложения, и зная как представлены числа на архитектуре предположить, что если у вас есть значение в переменной INT_MAX, вы к ней прибавите +1, то получите INT_MIN. А потом вы где-нибудь напишете if (X+n)>X, чтобы проверять не вызовет ли операция переполнение. И оно даже скорее всего так будет работать на протяжении многих лет. Но проблема в том, что стандарт языка не даёт таких гарантий! Вообще не даёт. Более того, согласно стандарту языка, такой код вообще не является корректным кодом. И если компилятор обнаружит а коде что-то подобное, то он имеет полное право в таком случае сгенерировать вообще все что угодно - вплоть до того, что этот if будет выдавать и true и false в разных частях кода для одинаковых значений X и N (пример там есть по ссылке), компилятор также что выкинуть какой-то находящийся рядом код или сгенерировать инструкции которые делают вообще не то что вы подразумевали. И то, что он не делает это сейчас, совершенно не гарантирует, что это не произойдет когда-нибудь в будущем (стоит вам обновить компилятор, изменить опции компиляции или даже поменять что-то в коде по соседству). А если у вас будет что-то типа такого в условии цикла, то бесконечный цикл может перестать быть бесконечным, или наоборот (см. пример).

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

Довольно подробные объяснения с примерами можно найти тут:

Неопределенное поведение и правда не определено

Неопределенное поведение может привести к путешествиям во времени

UFO just landed and posted this here

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

Нет, нет и ещё раз нет. UB не зависит от платформы или применения. Вообще не зависит - UB в коде либо есть, либо его нет, вне зависимости от платформы.

Случаи undefined behavior описаны в стандарте языка, их там несколько сотен. Код, в котором есть UB по определению не является корректным кодом на языке C или C++. Более того, в случае наличия UB в коде компилятор имеет полное право сгенерировать любую хрень из вашего кода, и это вообще не зависит от аппаратной платформы и ее ограничений.

Вы, кажется, не совсем верно понимаете, что такое UB. UB это не "я обращаюсь к неинициализированной переменной в куче, но на моей платформе память изначально заполнена нулями, значит в ней будет ноль". UB - это то, про что в стандарте языка явно сказано "так делать нельзя", и если компилятор увидит такое в коде, он, например, может вообще выкинуть условие if (a == 0) и все что внутри него. Имеет полное право. Если на вашей платформе инкремент знакового двухбайтового инта 32767 сделает его равным -32768 (на большинстве платформ так), то увидев if ((x+1)>x) в коде компилятор может выкинуть эту проверку и заменить везде на true, потому что согласно стандарту языка переполнение знаковых недопустимо. И так далее. Наличие или отсутствие UB в исходном коде не зависит от платформы, под которую вы компилируетесь.

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

UFO just landed and posted this here

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

Компиляторы с удовольствием пользуются некоторыми видами UB для оптимизации кода; в таком случае поведение остаётся неопределённым даже если зафиксировать платформу и версию компилятора.

По-моему в C изначально UB именно для этого и было и означало скорее implementation defined поведение. Т.е. язык как бы не при делах, но в конкретных условиях все ок. В те времена C еще можно было считать человекочитаемым ассемблером.

Сейчас же UB предназначено целиком и полностью для оптимизации, чтобы у компилятора была свобода ломать код так, как ему вздумается. Платформа здесь не имеет значения, потому что компилятор не смотрит на то, что в какой-то системе two's complement представление чисел. Для него UB это UB на любой платформе. Собственно, для него вообще платформы конечной не существует - он работает с абстрактной машиной. Поэтому как выше писали, проверка на переполнение будет просто удалена компилятором. Поэтому и версия компилятора может запросто сломать то, что работало раньше, если оно опиралось на UB.

оставляя конкретное поведение на совести компилятора.

Неа. То, что вы сказали - это не undefined behavior, а unspecified behavior, в стандарте оно тоже есть.

если конкретная платформа убирает UB из своего компилятора

Почти, обычно UB "убирает" не конкретная платформа, а конкретный компилятор. Например GCC и Clang делают исключение (точнее, расширение языка) для упомянутых выше union, разрешая в них puning простых типов. Но пользоваться таким можно только если разработчиками компилятора явно явно сказано "в стандарте C++ это UB, но мы реализуем нестандартное расширение языка, в котором это не UB". Если такого не сказано, то писать код таким образом под этот компилятор нельзя.

просто этот исходник не скомпилируется другим компилятором под другую платформу

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

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

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

Это только в том случае, если разработчики специально "определили" это конкретное UB и больше не считают его UB в своей реализации. А если в прошлой версии это UB было именно UB, то в новой версии тоже возможно все что угодно. То, что работало, может перестать работать. А может наоборот начать работать то, что раньше не работало :) на то оно и undefined

стоит проверить приведённую вами конструкцию UB в конкретной среде

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

Но пользоваться таким можно только если разработчиками компилятора явно явно сказано "в стандарте C++ это UB, но мы реализуем нестандартное расширение языка, в котором это не UB".

Это плохой пример из-за нелепости ситуации вокруг type punning'а через union. Комитет решил сломать совместимость* с C в этом месте, а разработчики (возможно, всех существующих) компиляторов - нет.

Если стандарт игнорирует реальность, тем хуже для стандарта, а не для реальности. Тут нужен ещё один пример.

Он также игнорирует реальность, когда требует поддержку исключений и RTTI во freestanding-реализациях (так на языке стандарта называется bare metal). Комитет почему-то изначально (в C++98) послал таким образом много платформ и там до сих есть люди, голосующие за сохранение ситуации**. @Tuvok спросил про Arduino, а самое первое, что можно сказать об avr-g++ и стандартности: стандарт игнорирует существование этой платформы (потому что не хочет учитывать существование -fno-exceptions).

Так проявляются некие трения насчёт совместимости с C и насчёт поддержки bare metal в комитете и в представлениях Страуструпа***. Можно с ними не согласиться, как и с мнением Страуструпа о C или C++ на 8-битных микроконтроллерах: "Probably best stick to assembler".

В общем, на это могут смотреть не как на явное расширение компилятора, а как на "вы не рискнёте по дурости комитета сломать весь софт, пользующийся этим приёмом: x265, Firefox, Qt, OpenCV, ClickHouse, Boost... (точно так же, как не добавите в компилятор для bare metal неотключаемые исключения ради соблюдения стандарта - это же глупо)".

* в стандарте C поведение описано на этой странице (ctrl+F: type punning), в стандарте C++ это уже UB.

** голоса 1 Against, 2 Strongly Against в голосовании "We support proposed removal..."

*** Страуструп 2002: "Remove all incompatibilities: This is my ideal", Страуструп 2024: "Unfortunately, unions are commonly used for type punning".

Для того, что вы описываете, в стандарте есть отдельное понятие - unspecified behaviour (да, тоже UB, но обычно когда пишут UB, подразумевают undefined behaviour). Наличие в коде unspecified behaviour означает потенциальные проблемы с переносимостью, но просто от обновления компилятора или изменения никак не связанного участка кода ничего не сломается.

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

интересно, а есть ли у кого-нибудь примеры, когда UB реально делало что-то ужасное..

А то всегда определяют UB собственно по его название - неопределенное, а примеров отформатированного диска не слышал никогда.

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

Почитал статью и так и не понял чем раст лучше, наверно это неплохой язык, но что мешает использовать пул потоков в с++, почему автор статьи считает что многопоточное программирование на с++ это потоки и мьютексы?

Мое понимание разработки на данный момент сводится к тому что можно очень долго выяснять какой язык лучше и чем на, скажем так, академических примерах, беда в том что это сильно далеко от реальной жизни. Мне не нужен язык, мне нужно программу делать, а для этого нужны библиотеки, очень много библиотек, на данный момент в составе vcpkg около 2,5 тысяч библиотек. Это код который у вас сразу и скорее всего без проблем соберется под все распространённые настольные и мобильные платформы. Не представляю что должно случиться что бы библиотека CGAL была переписана на другой язык. В нее закопаны наверно сотни человеколет работы. И это только одна библиотека из множества других. И такие библиотеки можно перечислять очень долго: SQLite, Qt, curl, boost graph, openimageio, eigen3, lua, imgui... У меня в глазах ужас при мысли как каждую из них можно было бы портировать на другой язык, не говоря уже о всех вместе.

В последнее время пишу в основном фронтенд на js. В реестре npmjs.org более 2 миллионов пакетов. Тем не менее пишу сложные веб приложения используя только 2-3 из них (кроме фреймворка), да и от тех при желании можно избавиться. Те, кто использует много библиотек через пару лет, имеет огромные проблемы с апгрейдом и поддержкой приложения

В С++ не так?

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

Кстати, раз уж про поддержку зависимостей заговорили. В Rust есть потрясающая возможность иметь одну библиотеку нескольких разных версий без конфликтов (если не надо между разными версиями взаимодействовать). Так что если ваша зависимость A имеет подзависимость C версии 1.0.0, а в зависимости B подзависимость C версии 2.0.0, то они спокойно продолжат работать. Можно даже импортировать свою собственную библиотеку другой версии (это используется, например, чтобы пофиксить баг только в самой новой версии, а в старых просто импортировать из новой и не поддерживать несколько версий фиксов).

Звучит, так, как будто это отличная фича, чтоб выстрелить себе в ногу.

Нет, на самом деле это офигенно и позволяет продолжать писать код, даже если прямая зависимость А уже обновилась чтобы использовать косвенную зависимость С версии 2.0, а зависимость B ещё на 1.0.

В плюсах это, я думаю, сразу ставит крест на такой комбинации - форкайте B и переписывайте на 2.0 или откатывайте А на старую версию.

Насколько я знаю, есть одну неожиданную проблему, которую это может вызвать - если все же библиотеки A и B обмениваются типами из C, то можно получить ошибку expected struct C::InnerType, found a different struct C::InnerType; note: perhaps two different versions of crate C are being used? В остальном, насколько я знаю, проблем это не вызывает.

Есть и вторая уже более интересная проблема. Все это ломается, когда надо цеплять библиотеку, зависимую от Си. Там нельзя иметь мультиверсию смешной зависимости.

Много раз сталкивался с такой проблемой в других языках.
Например библиотека А имеет зависиость на B 1.0, а C ссылается на B 2.0. Всегда было больно, из всех моих случаев поддержка разных версий зависимости решило бы проблему(в часте случаев так и фиксилось - пинилась старая версия какими-то костылями)

Проблема есть, я ж не отрицаю.

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

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

Нет, не так, с++ очень стабилен в поддержке и всегда имеет обратную совместимость(был один или два случая когда это правило нарушилось). Наверно есть библиотеки которые могут перестать работать, но таких случаев не помню. Многие с++ библиотеки слишком большие что бы быть заброшенными. Например curl это часть операционной системы линукс и с некоторых пор виндовс. Qt развивается с 93 года или что-то около того. Многие библиотеки с++ это матрешки, которые зависят от более мелких библиотек, а те еще от более мелких. К примеру openimageio зависит от 98 других библиотек. Так же в с++ очень мало зоопарка библиотек, никто не будет писать аналог zlib(библиотека сжатия данных).

Бегло посмотрел репозиторий что вы дали, нашел интересную штуку JoltPhysics.js( A WebAssembly port of JoltPhysics) это то о чем говорил, с++ проникает условно в мир других языков через порты и обертки под эти языки, уверен есть порт для питона и других языков, но внутри там будет с++ библиотека.

Еще почему-то в подобных разговорах принято считать что программисты с++ думаю только о нем и ничего вокруг не видят, в "мире с++" вполне нормальная практика брать и вкручивать в приложение другие языки, почти все игровые движки созданные на с++ имеют внутри себя интерпретаторы(или jit компиляторы) других языков для упрощения написания логики, как правило это lua или python, Unreal Engine внутри содержит графический язык BluePrint. Qt сейчас переходит на QML это использование с++ для базовых функции и склеивание их через js.

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

Мой пойнт был в том, что важных библиотек (типа графических или curl), которые используются в проекте - их очень немного. И они довольно оперативно и качественно портятся на другие языки/фреймворки когда надо. А "мелочь" можно реализовать самому.
Опять же - так в мире js и npmjs
То есть утверждение, что уже написано куча либ под что-то и что нам делать и будет куча проблем при переходе на Rust - необосновано

Почитал статью и так и не понял чем раст лучше, ..., но что мешает использовать пул потоков в с++

Видимо у меня не получилось расставить акценты в нужных местах. Преимущество Rust не в том, что есть библиотека, в которой пул потоков есть (и даже не в идиоматичном варианте, когда для того, чтобы сделать код многопоточным надо добавить .par_bridge().into_par_iter() и все), а в том, что компилятор гарантирует отсутствие гонок данных. Этот пример важен не самим кодом, а текстом после, т.е. пока я писал этот пример я сделал много разных ошибок, которые бы не помешали скомпилироваться C и/или C++ коду, но помешали скомпилироваться коду на Rust. Сила Rust в гарантиях компилятора, который позволяет экспериментировать. В safe невозможно скомпилировать программу там, чтобы передать одну задачу в 2 разных потока и потом дебажить гонки данных (можно послать копию задания, но это будет просто бесполезной работой, а не UB). Я могу позволить себе попробовать написать код без мьютексов, т.к.если я сделаю в нем ошибку, код просто не скомпилируется. В большинстве других языков конкурентность связана с огромным количеством граблей, которые приходится ловко обходить.

curl

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

SQLite

Не уверен, насколько они production ready, но на Rust пишут и базы данных, и key-value хранилища и прочие вещи, но сам язык в 3 раза моложе SQLite, так что трудно винить в этом язык.

Все же, "переписать все на Rust" это не то, что бы реальная цель или задача. Код, который полировали десятилетия никто просто так выкидывать не будет (уж точно не в первую очередь). Я поэтому и написал, что для меня у C и/или C++ ниша одна - легаси код, который слишком долго или дорого переписать.

в составе vcpkg около 2,5 тысяч библиотек

В Cargo.io на момент записи 141,981 пакетов. Это не честное сравнение, Rust поощряет разбиение пакетов на много мелких для переиспользования и ускорения компиляции, так что это не 141к обособленных пакетов, но все же.

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

Точно также можно в одну переменную писать и читать, пускай и через мьютекс.

Смотря что считать гонкой. Corrupt Shared State получить уже невозможно т.к. мютекс или другой какой атомик - и то хорошо. В плюсах это UB:

Critical race conditions cause invalid execution and software bugs. Critical race conditions often happen when the processes or threads depend on some shared state. Operations upon shared states are done in critical sections that must be mutually exclusive. Failure to obey this rule can corrupt the shared state.

A data race is a type of race condition. Data races are important parts of various formal memory models. The memory model defined in the C11 and C++11 standards specify that a C or C++ program containing a data race has undefined behavior.[3][4]

(c) https://en.wikipedia.org/wiki/Race_condition#In_software

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

Не невозможно. Не забываем про unsafe в Rust. Сложнее - да. Но не невозможно.

Кроме того, встречаются либы, которые просто обертки над C-либами. Что тоже не спасет от Corrupt Shared State. Впрочем, это тоже можно отнести к unsafe.

А вот свой код, особенно в рамках safe Rust - да, спасает от подобного. И это очень и очень круто. Потому как дебажить подобное очень сложно.

Гонка данных и состояние гонки это две разные проблемы, они даже подклассами друг друга не являются. Цитата из The Rustonomicon:

Safe Rust guarantees an absence of data races, which are defined as:
- two or more threads concurrently accessing a location of memory
- one or more of them is a write
- one or more of them is unsynchronized
A data race has Undefined Behavior, and is therefore impossible to perform in Safe Rust. Data races are mostly prevented through Rust's ownership system: it's impossible to alias a mutable reference, so it's impossible to perform a data race. Interior mutability makes this more complicated, which is largely why we have the Send and Sync traits.
However Rust does not prevent general race conditions.

Он строго гарантирует отсутствие гонок данных, но не может дать гарантии отсутствия гонок условий.

Не забываем про unsafe в Rust. Для unsafe никаких подобных гарантий нет.

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

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

Вот и ответ на вопрос "почему сторонники активно пытаются всё переписать на Rust?".

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

А ещё бывают баги в компиляторе.

Баги бывают во всех компиляторах.

И с Rust пока встречал баги компилятора пока только в ночных сборках (с WASM было связано).
В Golang находил багу в релизной версии (справедливости ради - то была бага в LSP, а не непосредственно в компиляторе).

И кстати, если вас хоть немного заинтересовал Rust, то еще раз рекомендую лекции Алексея Кладова. Там всего 13 лекций по полтора часа, но этого с головой хватит, чтобы понять что, как, зачем и почему в Rust. Особенно по сравнению с моими мыслями, которые я кое-как собрал в статью.

Алексей ведет потрясающий блог matklad (иногда на хабре публикуют некоторые посты из этого блога). В блоге много всякого про Rust и Zig. Рекомендую!

Зачем прямо сразу переписывать существующие библиотеки на Раст, если можно сделать обёртку и подтянуть как есть? SQLite, Lua, curl спокойно подключаются, imgui и openimage переписаны "по мотивам". Qt только не будет, слишком другой менталитет.

Qt только не будет, слишком другой менталитет.

Технически, биндинги к Qt - есть... Но я никому не пожелаю с этим работать)

Мне не нужен язык, мне нужно программу делать, а для этого нужны библиотеки, очень много библиотек,

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

Чем больше я читаю такие хвалебные оды расту и его преимуществам, тем сильнее у меня складывается ощущение, что фанаты раста, критикуя с и с++ за его возможность прострелить ногу, перестают понимать одну вещь - синтаксис с и с++ выстроен из неких математических абстракций вокруг архитектуры ЭВМ, он хоть и даёт возможность забыть про регистры, стек, прерывания и проч., но при этом он позволяет легко об этом вспомнить. Управление памятью, подход к разработке, определяемый компилятором - это всё наверное прекрасно, но кто уверен, что компилятор не ошибается? Ах,да, его пишут боги программирования, они никогда не ошибаются, ведь они пишут язык и компилятор, который никогда не ошибается. Возможно я сильно ошибаюсь, но разрабы, пишущие на расте, скорее всего не понимают, что там под капотом происходит, ведь "у нас самый надёжный супер пупер компилятор,который генерирует самый надёжный и безошибочный код". А потом окажется, через пять лет, что в компиляторе была дыра, которая генерировала код, в котором присутствовали сигнатуры, благодаря которым хакеры могли легко его реверсить и делать эксплойты. Да не, бред какой-то, это же самый надёжный и безопасный язык. Именно благодаря тому, что я на своих плюсах могу стрелять в ногу, и регулярно это делаю, я знаю как сделать так, чтобы мне никто другой не смог выстрелить в ногу. Ничто не совершенно, никто не совершенен.

Дыру в Rust-компиляторе найдут и исправят, фикс автоматически применится для всех Rust-программ, собранных новым компилятором.

Миллионы выходов за пределы массива, use-after-free и прочих UB в миллионах сишных программ не найдут и не исправят никогда.

могли легко его реверсить и делать эксплойты

Да вперед, большая часть кода, где "делают эксплойты" вообще в опенсорсе.

разрабы, пишущие на расте, скорее всего не понимают, что там под капотом происходит

Так в этом и плюс Rust,ты можешь позволить себе не знать, что там происходит (и использовать Rust как Python 4) и при этом писать быстрый и корректный код.

синтаксис с и с++ выстроен из неких математических абстракций вокруг архитектуры ЭВМ

Вокруг PDP-11, если быть точным

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

Зачем мне вспоминать про архитектуру PDP-11? Какое отношение она имеет к современному аппаратному обеспечению c многоядерными процессорами, алгоритмами когерентности кэша, спекулятивному выполнению и тому подобному?

Спойлер - никакого.

но разрабы, пишущие на расте, скорее всего не понимают, что там под капотом происходит

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

А потом окажется, через пять лет, что в компиляторе была дыра

А потом окажется, через пять лет, что современный процессор - это не одноядерный PDP-11, во что свято верили поклонники мантры "си - это низкий уровень" и произойдет Spectre & Meltdown. Oh shi******

C Is Not a Low-level Language, your computer is not a fast PDP-11

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

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

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

Всё УБ от оптимизаций на предположениях. С отключением оптимизации будет выполняться то, что написано.

Всё УБ от оптимизаций на предположениях

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

Ну вот я пишу без оптимизации на C89 для промавтоматики и мне важно чтобы выполнялось всё как написано в коде. Как раз и медленно и безопасно!

А зачем вообще выбран Си, если нужно "медленно и безопасно"? Там и кроме UB ногострелов достаточно же...

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Люди, даже имеющие огромный опыт, ошибаются. Особенно легко ошибиться при рефакторинге. Если какие-то свойства программы можно проверить автоматически - лучше это сделать автоматически, и чем раньше, тем меньше стоимость ошибки. Именно поэтому даже в традиционно динамических Python и JS (в виде TypeScript) добавляют аннотации типов. Именно поэтому в Расте существует safe подмножество языка, в котором можно спокойно мешать код и не бояться о тысячах способов получить UB. Именно поэтому индустрия стремится отойти от языков со слабой типизацией навроде Си.

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

Отличное описание Си, но что же хорошего в этой маскировке?

И любое некорректное обращение будет вызывать системное исключение "выход за границу объекта"

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

Люди, даже имеющие огромный опыт, ошибаются. Особенно легко ошибиться при рефакторинге. Если какие-то свойства программы можно проверить автоматически - лучше это сделать автоматически, и чем раньше, тем меньше стоимость ошибки. Именно поэтому даже в традиционно динамических Python и JS (в виде TypeScript) добавляют аннотации типов. Именно поэтому в Расте существует safe подмножество языка, в котором можно спокойно мешать код и не бояться о тысячах способов получить UB. Именно поэтому индустрия стремится отойти от языков со слабой типизацией навроде Си.

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

Или нет таких трудностей, которые мы не смогли бы себе создать. За 30+ лет в разработке в этом убедился многократно.

Отличное описание Си, но что же хорошего в этой маскировке?

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

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

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

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

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

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

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

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

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

UFO just landed and posted this here

Зато это соотношение меняется на 70 к 30, когда кто-то активно ищет колдунские проблемы в вашем приложении.

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

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

И после этого вы ещё заявляете о вреде проверок границ, которые приходится отключать в тяжёлых расчётах?!..

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

язык не должен исполнять роль нянечки в детсаду

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

любой объект в системе должен иметь "операционный дескриптор"

Только систем таких нет. В Rust как раз по умолчанию все это и проверяется (как на этапе компиляции, так и в рантайме).

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

Вы всё ещё можете отключить "нянечку в детсаду", но только при этом вам придётся подумать -- а почему это мне приходится так делать? Обосновано ли это? Не подкладываю ли я себе свинью на будущее? А потом обклеить опасный участок яркой лентой и поставить табличку "осторожно, скользкий пол", чтобы в будущем было проще искать такие места.

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

Что характерно, защищённый режим x86 как раз про это. Но никто не заценил фишку, не воспользовался ей, не создал ни ЯП, ни ОС, которые бы использовали это в полной мере. А затем и Intel забросила и похоронила концепцию.

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

И была бы аппаратная защита от выхода за границы массивом и буферов. Buffer overflow attack не было бы как класса. Атак с шеллкодам не было бы как класса.

Я немного про другое.

Вот, например, получили вы некоторый указатель аргументов функции. И представьте, что у вас есть некое системное API которое позволяет получить информацию о том, на что этот указатель указывает - тип объекта, размер памяти, который он занимает... Т.е. к каждому объекту есть еще "операционный дескриптор" (operational descriptor, opdesc) по которому можно получить некоторую интересную информацию.

Единственное - чтобы это использовать, нужно я вно указать компилятору что требуется передача операционных дескрипторов в функцию. Для С/С++ это будет

int func (char *, int *, int, char *, ...); /* prototype */
#pragma descriptor (void func ("", void, void, ""))

В данном случае для аргументов char* внутри функции всегда можем посмотреть - а что на самом деле за ними кроется - какого размера буфер?

В других языках иначе - в некоторых просто достаточно модификатора OpDesc в прототипе функции.

Тогда это половинчатое решение какое-то...

Ваш #pragma descriptor требует пояснения: я не понял, почему в прототипе int (тип возврата), а в прагме void, что означают пара пустых строковых литералов?

И ещё: в вашей концепции запрашивать информацию о размере буфера это обязанность программиста или это неявно будет делать компилятор?

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

Вы теперь мой любимый комментатор на Хабре: ходите по всем статьям, где упомянут C/C++, и вбрасываете порцию токсичности. Через пару комментариев не забудете упомянуть, что вы консультант по разработке и должность придумали специально для вас. А также, что вы каждый день работаете с кодом, который не правился 10 лет (видимо, на него смотрите) и ещё пару раз оскорбите программистов, пишущих на чём-то, кроме вашего любимого языка.

Так и день пройдёт)

Где вы увидели токсичность в адрес С/С++?

Написано ровно то, что написано. И ничего более. Додумать можно все что угодно, но это уже из разряда "сама придумала - сама обиделась".

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

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

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

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

Где вы увидели токсичность в адрес С/С++?

Вы невнимательно прочитали: я как раз написал, что ваша токсичность направлена на людей, не пишущих на вашем любимом языке. Можно вспомнить, как вы пренебрежительно относитесь к "перекладывателям джейсонов")

Есть некоторая предвзятость к людям

Именно.

начинают с пеной у рта доказывать

Никакой пены у рта у автора не замечаю. Человек делится своими впечатлениями, притом честно даёт представление о своих компетенциях, чтобы мы могли адекватно оценить подаваемый материал. Он же не пишет: "я 20 лет писал на C, но в конец задолбался и перехожу на Rust, так как в очередной раз вылетел за границы массива", верно?

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

А почему тогда не Pascal, Oberon или Ada?

B&R любит С. Да и всякие PC-based тоже. Но при наличии культуры кода, руководств, правильных ограничений и отсутствия всяких "смотри как я могу", получается легко читаемый структурированный код.

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

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

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

Всё УБ от оптимизаций на предположениях. С отключением оптимизации будет выполняться то, что написано.

Покажете параграф в стандарте языка, гарантирующий это?

А покажите параграф в стандарте где должно выполняться что-то иное в противовес к директивам программы?

А покажите параграф в стандарте где должно выполняться что-то иное в противовес к директивам программы?

Не "должно", а "может". Параграф 3.64:

Permissible undefined behavior ranges from ignoring the situation completely with unpredictable results, to behaving during translation or program execution in a documented manner characteristic of the environment (with or without the issuance of a diagnostic message), to terminating a translation or execution (with the issuance of a diagnostic message).

То есть прямым текстом стандарт допускает что в случае undefined behaviour может быть совершенно unpredictable результат - в случае UB стандарт разрешает компилятору сгенерировать вообще все что угодно, даже то, чего нет в "директивах программы". Каких-либо исключений из этого правила в случае отключенных оптимизаций компилятора там нет, никаких гарантий не даётся.

А ещё есть замечательный параграф 4.1.2, где сказано

A conforming implementation executing a well-formed program shall produce the same observable behavior as one of the possible executions of the corresponding instance of the abstract machine with the same program and the same input. However, if any such execution contains an undefined operation, this document places no requirement on the implementation executing that program with that input (not even with regard to operations preceding the first undefined operation).

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

Ну так это если вы сами УБ запрограммировали, то и получаете неопределённое. Тут вообще без вопросов! А вот если программировать правильно, то никакого левого поведения не предполагается.

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

Конечно из-за оптимизаций! Программист пишет без указателей, мусор не создаёт, память выделяет при старте, границы проверяет.

Ответ неправильный.

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

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

Всегда можно сказать "нужно программировать правильно, а неправильно программировать не надо", в стандарте C++ описано несколько сотен случаев, вызывающих UB, на почти тысяче страниц. Они далеко не только про память, указатели, мусор и границы, вообще нет. Многие из них совершенно не очевидные (их практически невозможно вывести логически, нужно именно внимательно читать стандарт и знать, что то или иное вызывает UB).

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

Многие из них совершенно не очевидные

Например?

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

long i;
float y;
y  = number;
i  = *(long*)&y;

Или сделали то же самое через union, как часто делают:

union myunion {  
  float f;
  long l;
}
myunion.f = number;
/* тут используете myunion.l */

Казалось бы, все прилично. На вашей платформе размеры long и float равны. У вас один поток. Вы не выделяете память в куче, только на стеке. Вы не выходите ни за какие границы. У вас нет никакого переполнения. У вас нет никакой утечки. Но в Си первый код содержит UB, а в Cи++ и тот и тот код содержат UB. Потому что в первом случае нарушается правило strict aliasing, которое вы обязаны знать, во втором случае нарушается active member rule, которое вы тоже обязаны знать.

Пример с проверкой на переполнение вида if ((X+n) > X) ниже уже разбирался. Если X - signed, то компилятор имеет право выкинуть все такие проверки, заменив их на рандомные true и false в разных частях кода. Вы-то знаете, что для вашего процессора 0x7FFF + 1 = 0x8000, что на вашей платформе будет 32767 и -32768 соответственно, но вот компилятор имеет полное право на это наплевать и скомпилировать что угодно вместо вашего условия. Потому что это не unspecified, а именно undefined behavior.

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

Ну вот дичь же! Это же точно из серии "смотри как я могу". Можно еще через двойную косвенную адресацию провернуть финт. Но нафига?!

преобразование i = (long)y; разве не покатит чтобы разместить значение в переменной большей разрядности?

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

И зачем вообще заигрывать с адресацией *&var без причины? Байты в слове попереставлять красиво?

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

UPD: хотя, постойте, i = (long)y; не покатит. Вы же хотите IEEE 754 float побайтово с мантиссой, экспонентой и всем знаками переместить в long. Но и тут вопрос, а что дальше то? Руками будете парсить, CRC считать? Тогда лучше читать память через memcpy и sizeof.

Ну вот дичь же! Это же точно из серии "смотри как я могу". Можно еще через двойную косвенную адресацию провернуть финт. Но нафига?!

преобразование i = (long)y; разве не покатит чтобы разместить значение в переменной большей разрядности?

Нет, не прокатит, оно преобразует число из флота в инт математически, а надо побайтово. Количество байт и там и там одинаковое.

Но и тут вопрос, а что дальше то?

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

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

Тогда лучше читать память через memcpy и sizeof

Да, это единственно правильный вариант, не вызывающий UB в C ++17 и старше. В C++20 добавили ещё bit_cast. Но программист, не знающий про strict aliasing напишет простой каст как в примере выше и получит UB.

Если компилятор заменит проверку if ((X+n) > X) на if (true),

Ха-ха, нет, все ещё хуже. Компилятор может заменить эту проверку не только на true, но вообще не все что угодно. Вплоть до того, что в одном месте он ее заменит на true, а через несколько строчек в другом месте такую же проверку заменит на false. Вот тут об этом есть.

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

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

Регистры четырехбайтовые, то есть uint32_t, но конкретно в некоторых регистрах в памяти того внешнего устройства лежат float, и вам нужно работать с ним как с float.

Будем считать, что они у вас отображены на память и адрес вы знаете, например uint64_t addr = 0x0e55eeff8f4;

float* pf = (float*)addr; - вот указатель на ваш регистр как на float, даже memcpy не нужен.

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

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

мне нужна предсказуемость, а не дичь.

Значит С не для вас :)

Будем считать, что они у вас отображены на память и адрес вы знаете, например uint64_t addr = 0x0e55eeff8f4;

Ахахаха, с чего вдруг "будем считать"-то? Ничто никуда по умолчанию не отображено, а сторонняя библиотека выдает вам только регистры uint32_t или массив из таких регистров. Да то там говорить, самая популярная в мире библиотека работы с Modbus libmodbus читает значения регистров только в буфер uint16_t*. Вы прочитали два регистра, и теперь вам надо перевести их во float.

вот не нужен мне такой компилятор

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

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

Вместо железа в памяти у вас значения в памяти... Какая разница в плане чтения из адреса? Можете через void* привести к float*. Страшно? Прочитали два двухбайтовых регистра, и теперь вам надо перевести их в float? Пихните прочитанные двухбайтовые значения в 4х байтовую переменную с учетом endianess (тут прям открывается свобода для программиста) и кастаните к float через reinterpret_cast. Фактически, вы говорите компилятору, что хотите работать с этими четырьмя байтами в памяти как с типом float. С чего вдруг УБ нарисуется?

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

Можете через void* привести к float*

Не поможет, у вас все равно будет UB. Прочтите внимательно пункты 6, 7 параграфа 6.5 стандарта.

Пихните прочитанные двухбайтовые значения в 4х байтовую переменную с учетом endianess (тут прям открывается свобода для программиста) и кастаните к float через reinterpret_cast.

Это тоже UB:

Whenever an attempt is made to read or modify the stored value of an object of type DynamicType through a glvalue of type AliasedType, the behavior is undefined unless one of the following is true:

  • AliasedType and DynamicType are similar.

  • AliasedType is the (possibly cv-qualified) signed or unsigned variant of DynamicType.

  • AliasedType is std::byte, (since C++17) char, or unsigned char: this permits examination of the object representation of any object as an array of bytes.

Вот видите, вы в своих предложениях нарвались на UB. Это было совершенно очевидно, ведь правда? :)

С чего вдруг УБ нарисуется?

С того, что согласно стандарту языка это UB, и стандарт не даёт гарантий генерации корректного кода компилятором после этого. Ещё раз говорю, не путайте undefined behavior и unspecified behavior.

Мне не стандарт надо знать до мелочей, а целевую платформу.

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

Если уж Си компилятор будет упрямиться, то получит ассемблерную вставку и делов

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

Whenever an attempt is made to read or modify the stored value of an object of type DynamicType through a glvalue of type AliasedType

Я правильно понимаю, что memcpy из int_32* в float* (в смысле, копирование четырёх байт, на которые указывает первый указатель, по адресу, на который указывает второй) сюда тоже подпадает? Или как раз это допустимо? Просто если это допустимо, тогда не совсем понятно, почему reinterpret_cast из int_32 в float (не между указателями, а именно самого значения, как transmute в Rust) недопустим - семантически-то, по идее, это то же самое: "читаем байты как тип x [которым они и являются - всё корректно], записываем в другое место как y [и x тут уже не при делах]".

Это цитата была конкретно про reinterpret_cast.

С memcpy объекта у вас уже два разных, алиасинга нет. Грубо говоря, при касте у вас сначала происходит доступ к объекту (по не-его типу), а потом копирование, а при memcpy сначала копирование, а потом доступ (по его типу).

В самом memcpy ничего криминального, у него "Both objects are interpreted as arrays of unsigned char", а с чарами стандарт явно разрешает алиаситься.

Если интересная тема, есть очень хороший разбор со ссылками на стандарты C и C++.

Если язык мне разрешает делать такие базовые вещи, при этом стандарт компиляции берет кардбланш на дичь, то тут что-то не так со стандартом, т.к. с языком всё норм последние 50 лет.

Если завтра комитет объявит УБ вообще всё, то компилятор можно будет сократить до генератора случайного кода!

С чего вы взяли, что "язык вам это разрешает"? Язык - это то, что описано референсной документации для него, в случае с C и C++ это утвержденный комитетом стандарт ISO. И он вам эти вещи делать не разрешает, там об этом прямо написано, что так делать нельзя ни в коем случае. Вы же, видимо, просто плохо изучили язык, ваш основной рабочий инструмент, раз об этом не знаете.

Если какая-то реализация языка C или C++ не соответствует стандарту, то не надо называть это C и C++. Если вы компилируете код компилятором, который соответствует стандарту (GCC, Clang, Keil, и многие другие), то не надо удивляться, что они следуют правилам и ограничениям, описанным в стандарте.

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

То есть вы серьёзно считаете такое неадекватное поведение компилятора приемлемым... Дожили...

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

Это мир не языка Си, а мир Стандарта, которого изначально не было и всё прекрасно компилировалось в соответствии с языковыми конструкциями. Комитет пошёл на поводу у каких-то хмырей далёких от программирования и превратил низкоуровневый Си в высокоуровневый Си-хрен-пойми-что, на котором нормально даже драйвер не написать, если следовать современным стандартам. Так что компилятор обязан, а иначе будет заменён на тот, который при виде УБ не будет вырезать кусками код, нарушая структурную целостность программы, да так, что программный счётчик попадает в функцию, которую не вызывали!

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

Комитет пошёл на поводу у каких-то хмырей далёких от программирования

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

В комитете по стандартизации C++ кроме остальных сам Бьярне Страуструп заседает. Вам, конечно, безусловно виднее, как ему стоит развивать созданный им же язык :)

котором нормально даже драйвер не написать, если следовать современным стандартам.

Что для вас "современным"? В сях подобные правила там ещё с начала 90-х годов, в плюсах с самых первых версий стандарта. И да, драйвер вам писать никто не мешает. Просто делайте это так, как вам предписывает используемый вами язык программирования, а не как курица лапой "авось прокатит". А, ну да, для этого нужно сначала этот язык изучить, одного только знания "целевой платформы" для разработки, как мы выяснили, совсем недостаточно.

Так что компилятор обязан, а иначе будет заменён на тот, который при виде УБ не будет

Компилятор вам обязан только то, что гарантировали его разработчики (то есть, реализацию стандарта языка, ну ещё могут быть какие-то нестандартные расширения). Кстати, а вы уверены, что такие существуют? Вот прямо со стопроцентными официальными гарантиями от производителя, что у этого конкретного компилятора нет undefined behavior, а есть только unspecified behavior, и что из некоторректного кода он всегда будет генерировать корректную программу? Поделитесь ссылкой, если знаете такой?

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

Вы тут теоретизируете про рабочий инструмент и подход студента первокурсника, а в проблему погрузиться отказываетесь. Безусловно, стандарт в той части, что описывает язык Си, и есть язык Си. А вот в той части, что описывает концепцию УБ, это знатный косяк, которым воспользовались разработчики компиляторов, чтобы тупо выпиливать куски кода на основании заключения о УБ как о невозможном. В результате компилятор такой: "у вас тут указатели сравниваются на разные области памяти, а это УБ, а значит такого не может быть никогда (сфигали?) и можно код порушить вместо того, чтобы в инструкцию CMP преобразовать". Или компилятор вдруг узнал, что знаковое целое переполняется, так что инструкции ADD вы не дождётесь. Ну бред же! Ну или священный нулевой указатель, разыменовав который можно вызвать ледниковый период вызвать, вместо того, чтобы отдать данные по адресу 0x0000. То есть договор с современным компилятором о генерации понятного платформозависимого кода больше не работает. Вместо этого работает "вижу УБ - делаю любую дичь". А вы тут, ой, как же такое случилось, тупые эмбеддеры не знают свой инструмент... Они его слишком хорошо знают, в том то и дело.

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

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

Они его слишком хорошо знают, в том то и дело.

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

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

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

Разработка стандарта языка всегда основывается на компромиссах. Если требование "переносимости" какой-то фичи, актуальной только на редких и специфичных платформах сломает кучу оптимизаций на других более популярных платформах, то скорее всего ее так и оставят UB. А разработчики компиляторов под конкретно те платформы сделают у себя в компиляторах расширение языка, в котором конкретно это UB не UB.

Но в этом контексте не понятен наезд именно на "современные" стандарты. Упомянутый выше запрет развменвывания нулевого указателя был в сях ещё с 70-х годов (почему именно - вопрос отдельный, скорее всего какие-нибудь исторические причины типа наследия PDP-11), это не специфика новых стандартов. Правило о том, что в случае наличия в коде UB корректность итоговой программы не гарантирована, там тоже с лохматых времён, первые версии GCC в 90-х годах вообще "Ханойские башни" на девелоперской машине запускали, когда обнаруживали в коде UB :) Короче, тут не понятен именно хейт "новых стандартов", потому что все, на что жалуется комментатор выше, было ещё и в очень старых. Я бы даже сказал наоборот, новые стандарты в этом плане лучше, в C++20 вон, например, придумали переносимый bit_cast наконец-то.

Ну хоть теперь, оглядываясь в прошлое, вы поняли, что внедрение концепции УБ поверх языка, на деле оказалось плохой идеей?

Вы так пишете, как будто мы их внедряли.

первые версии GCC в 90-х годах вообще "Ханойские башни" на девелоперской машине запускали, когда обнаруживали в коде UB

Побуду занудой: там было unspecified behaviour, то есть определяемое платформой, а не стандартом. А конкретно обработка директивы #pragma. UB (undefined behaviour) за исключением очевидных частных случаев вроде int a = *(int*)NULL; вот так вот просто детектировать при компиляции нельзя (было бы иначе - это поведение бы уже давно стандартизировали как ошибку компиляции).

вы серьёзно считаете такое неадекватное поведение компилятора приемлемым

Это не автор считает, это стандарт так считает.

О, кстати, я вам тут теорию подкину. А вы разве никогда не задумывались, почему стандарт стал считать за УБ именно то, а не что-то другое? Зачем в стандарт наваливают все больше и больше положений об УБ? Сам язык Си имеет невысокий порог входа, он структурирован и строг. Зато карта минных УБ полей на тыщщу страниц, а главное священное знание и опыт, как её читать создает вокруг языка орел непреступности. Ведь новые модные языки продвигаются не просто так. Ведь клич "А давайте перепишем всё на раст" дорого стоит! А именно - инвестиции, рабочие места, обучение, контракты, субподряды и распилы мегатонн человекочасов. И те хмыри в комитете далёкие от программирования вполне могут быть очень близки к финансам. Просто программирование перестало быть приоритетом.

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

Ну что вы как маленький, всё вам объяснять надо. На самом деле, раст появился ещё тогда, просто тайная жидомасонская ложа решила придержать его якобы изобретение до нужного момента, чтобы испорченный си создал потребность, и вам можно было продать раст! /жирный_сарказм

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

определялся платформой

Когда что-то определяется платформой, это не undefined, а unspecified или implementation-defined behavior, это разные термины.

А фраза про то, что undefined behaviour может привести к "...ignoring the situation completely with unpredictable results" (то есть UB в сегодняшнем понимании) была ещё в стандарте Си 1989 года, с тех пор ничего в этом плане не изменилось.

С UB встречаешься обычно при смене компилятора / платформы / архитектуры.
Если одним компилятором компилировать под тот Win x64, то шансы встретить UB пускай и есть, но они практически все всплывают еще на этапе тестирования.

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

Таки Rust лучше C/C++?

Слушайте, ситуация с Rust напоминает ситуацию с ЛГБТ-повесточкой.

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

Ситуация действительно как с лгбт-повесточкой, а именно: в оьоих случаях противники путают информацию с агитацией. Каким образом статья на хабре может заставить вас писать иначе? Это же просто информация.

Ну да. Я ведь ровно о том же.

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

P.S.

И еще - я вместо "противники", писал "молодцы". Что уже само по себе характеризует отношение а оппоненту. Разве нет?

Аргумент одинаковый — если у вас достаточно неокрепший(детский/подростковый/студенческий в случае Rust) ум, то вас легко направить по тому или иному пути. Если вы 40-летний сеньор, то радужный раст вам нестрашон:)

Много с чем ситуацию напоминает - "холивар" это называется. Было всегда и будет ещё.

Ну да. Лучший инструмент - это тот, которым ты сегодня можешь делать продукт.

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

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

Rust Embed вполне активен насколько мне известен и опять же эргономика и инструментарий этой экосистемы в среднем лучше чем альтернативы для C++, хотя эквивалентный код вполне можно было написать и на C++. Т.к. работа с платами в любом случае будет сопряжена с манипуляциеми с голой памятью, то на определённых уровнях так или иначе всплывёт unsafe и возможность скрафтить UB, так что с плюсами в этом плане различий меньше. Оно будет просто удобнее в среднем.

наверняка на этот счет есть линтер, но такой код просто не должен компилироваться. А про очевидность поведения zero value для разных типов мне даже говорить не хочется

Вот тут есть чоткое непонимание ситуации. Цель в том, что новое поле всегда может найтись и оно будет нулевым. Приползёт от старого сервиса. Очнётся зомби с кодом недельной давности. Придётся откатить один из сервисов. Надо прочитать файл который создали год назад. И так далее. То есть "новое поле == нечто нулевое" это норма с которой должен работать любой код. Нельзя "не скомпилировать", потому что это нормальное поведение рантайма.

Если мы поддерживаем старое API, то можем сделать это поле опциональным и обрабатывать надлежающим способом. А вот уже другие ситуации, по моему мнению, стоит рассматривать отдельно, иначе можно получить неприятные ошибки.

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

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

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

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

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

В общем случае это не так и для Раста

  1. #[derive(Default)]

  2. FooBar { foo: 42, ..Default::default() };

И добавление поля так же закончится нулем или что еще хуже - false , потому что строка 2 спокойненько скомпилируется

В общем случае это не так и для Раста

Назвать это общим случаем это, конечно, громко сказано.

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

  • надо явно сказать "я хочу для всех остальных полей значения по умолчанию".

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

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

Или разница в поведении слайсов и мап. Nil слайс на практике от слайса нулевой длины ничего не отличается. Nil мапа же почему-то не может при записи аллоцировать память и теперь у такой мапы есть 2 возможных использования: 1) узнать, что ключа (любого) в этой мапе нет; 2) получить панику при попытке ключ записать. И такой потрясающий объект можно вернуть из функции ничего не подозревающему пользователю, вот он то обрадуется.

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

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

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

Я потому и написал, что это субъективное суждение (меня и некоторых моих друзей). Вполне допускаю что это ошибка выжившего. Я не часто по работе пересекался с Rust программистами, но был один совместный проект в пару лет длиной. Не могу чётко сформулировать свои претензии (оценка токсичности вещь относительная), но большая часть rust программистов с которыми я сталкивался оказывались не в меру высокомерными и слишком большими адептами из разряда «кроме раста другие языки не нужны», «если всё переписать на раст, то мир станет лучше», «вы тупые потому что вы не понимаете прелесть раста», «ваш с++ говно» и так далее, в таком духе.

Вот в чем точно "недостаток" (сами решайте, насколько это действительно недостаток) так это в том, что он заставляет очень критически относится к другим языкам.

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

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

Я вот наоборот, с Golang работаю после Rust (правда c Golang познакомился задолго до Rust).
И с одной стороны - действительно, Rust был гораздо более выразительным и первое время было сложно. А с другой - читать/изучать код стало гораздо легче.

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

P.S. подобный эффект есть и если после Golang сесть за Java. Java кажется крайне многословной.

У всех свои приоритеты, мне после Rust и про Python сильно долго думать не хочется, в то тоска одолевает. Я понял, что Golang мне абсолютно не нравится когда проходил A Tour of Go и за сколько там упражнений нашел 4 возможности отстрелить себе лишнюю конечность:

  • возможность читать слайс после длины;

  • nil map, которая не позволяет делать с собой ничего полезного (хотя nil слайс на практике идентичен слайсу нулевой длины);

  • поведение nil и закрытых каналов;

  • возможность переопределять ключевые слова (знаменитое #define true (rand() > 10) //Happy debugging suckers).

Забыли ещё нетривиальное взаимодействие между append и изменением элементов по индексу

возможность читать слайс после длины;

Можете пояснить, о чём тут речь? Вроде же, если просто индекс вылезает за границы слайса - мы сразу получаем панику, как и в Rust?

возможность переопределять ключевые слова

Опять же, это, видимо, какой-то такой ногострел, с которым я пока не сталкивался, поясните?

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

Чтение слайса после длины
package main

import "fmt"

func main() {
    s := []int{2, 3, 5, 7, 11, 13}
    printSlice(s)

    // Slice the slice to give it zero length.
    s= s[:0]
    printSlice(s)

    // Extend its length.
    s = s[:6]
    printSlice(s)
    
    s= append(s, 7)
    printSlice(s)
    
    // Works just fine
    d := s[0:cap(s)][11]
    fmt.Printf("len=%d cap=%d d=%v\n", len(s), cap(s), d)
    
    // Obviously this panics
    g := s[11]
    fmt.Println(g)
}

func printSlice(s []int) {
    fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s[0:cap(s)])
}

С помощью такой техники можно создавать очень веселые гайзенбаги

package main

import (
	"fmt"
	"math/rand/v2"
)

type SlidingWindow struct {
	s      []int
	offset int
	size   int
}

func (w *SlidingWindow) next() []int {
	if w.offset > (len(w.s) - w.size + 1) {
		return nil
	}
	t := w.s[w.offset : w.offset+w.size]
	w.offset += 1
	return t
}

func sliding_window(s []int, offset int, size int) []int {
	return s[offset : offset+size]
}

func main() {
	s2 := []int{1, 2, 3, 4, 5, 6}
	if rand.IntN(10) > 5 {
		s2 = append(s2, 7) // panics without append
	}

	sw := SlidingWindow{s2, 0, 3}

	for w := sw.next(); w != nil; w = sw.next() {
		fmt.Println(w)
	}
}
Переопределение ключевых слов
package main

import (
	"fmt"
	"math/rand/v2"
)

func really_big_func() (a, b bool) {
	// a lot of code
	// Happy debugging, suckers 
	true := rand.IntN(10) > 5
	false := rand.IntN(10) > 3
	fmt.Println(true)
	fmt.Println(false)
	// a lot of code
	return true, false
}

func main() {
	fmt.Println(really_big_func())
}

Погодите, в go операция вырезания куска массива-слайса может этот слайс расширить?! И это не баг, а документированное поведение?

Да какого хрена?!

Кстати, вот пример короче и показательнее
package main

import "fmt"

func main() {
	s := []int{1, 2, 3, 4, 5, 6}
	fmt.Printf("%v\n", s) // [1 2 3 4 5 6]

	s = s[1:4]
	fmt.Printf("%v\n", s) // [2 3 4]

	s = s[1:4]
	fmt.Printf("%v\n", s) // [3 4 5]
}

Погодите, в go операция вырезания куска массива-слайса может этот слайс расширить?! И это не баг, а документированное поведение?

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

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

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

Я знаю, что массив - не слайс, но:

  1. во-первых, слайсы играют в Go ту же роль, которую играют массивы в программировании в целом;

  2. во-вторых, в выражение s[1:4] синтаксически выглядит как вырезание куска из слайса s, а не из скрытого буфера за ним.

...мда, спасибо, первое действительно проклято. Второе-то ладно, просто кто-то явно забыл про то, как JavaScript прошёлся по граблям с переопределением undefined...

В этом моя проблема с Golang, они там очень много чего "забыли","не знали" или "проигнорировали". Не то, чтобы я считал себя умнее создателей Golang, они явно делали язык под свои конкретные нужды, но когда рандом с улицы (я) может за 5 минут найти дыру в языке, который только начал изучать, то это как-то не очень.

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

Слайс -- это тип-надстройка над массивами. Не сами массивы. Это как view таблицы в базе данных.

Так что, что такое "true" в любом случае будет видно сразу.

Только что проверил, golangci-lint run игнорирует такой код (golangci-lint has version 1.55.2):

package main

import (
	"fmt"
	"math/rand"
)

func main() {
	true := rand.Intn(10) > 5
	if true {
		fmt.Println("randomly true")
	}
}

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

Это как view таблицы в базе данных.

Это замечательно, только вот как мне это поможет избежать (это хотя бы можно s[:3:3] починить):

https://go.dev/play/p/7dkrDMD4v9D

package main

import "fmt"

func main() {
    s := []int{2, 3, 5, 7, 11, 13}
    printSlice(s)

    s = s[:3] // limiting len
    printSlice(s)
}

func printSlice(s []int) {
    s = s[0:cap(s)] // random func ignores limit
    fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}

Или такой гайзенбаг, когда то, упадет код или тихо будет работать неправильно зависит от того, был ли слайс переалоцирован или нет:

https://go.dev/play/p/Fud-Dmdgq1u

package main

import (
	"fmt"
	"math/rand/v2"
)

type SlidingWindow struct {
	s      []int
	offset int
	size   int
}

func (w *SlidingWindow) next() []int {
	if w.offset > (len(w.s) - w.size + 1) {
		return nil
	}
	t := w.s[w.offset : w.offset+w.size]
	w.offset += 1
	return t
}

func sliding_window(s []int, offset int, size int) []int {
	return s[offset : offset+size]
}

func main() {
	s2 := []int{1, 2, 3, 4, 5, 6}
	if rand.IntN(10) > 5 {
		s2 = append(s2, 7) // panics without append
	}

	sw := SlidingWindow{s2, 0, 3}

	for w := sw.next(); w != nil; w = sw.next() {
		fmt.Println(w)
	}
}

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

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

Вам бревно в глазу не мешает?

Нет, не мешает, я разобрался, как что работает. Peace, bro!

Я бы ещё добавил любовь к "программированию в комментариях". Вот уж просто минное поле.

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

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

Тут скорее зависит не от факта согласия-несогласия, а от того, в какой форме он подается

А с формой-то что не так?

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

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

Столь радикальные заявления это какой то детский максимализм или рвение неофитов, и у кого то вызывает лишь улыбку, а у кого то сомнения в адекватности этих коллег...

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

Можно ли парсер логов сделать на расте? Да. Он будет быстрым? Да. Будет ли он столь же понятен изящен и лаконичен как на питоне или прости господи perl. Полагаю нет.

Можно ли обработчик прерывания написать на раст? Можно (спасибо Филиппу Опперману). Будет ли он лучше в этом обработчика на С или ASM? Нет.

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

Будет ли специалист по нейросетям, разрабатывать нейросети на rust?

Работаем потихоньку, вот примеры от самых хайповых компаний индустрии: https://github.com/openai/tiktoken, https://github.com/huggingface/candle, и в целом. Не смотря на все заявления "да питон там только клей для сишных библиотек, он ни на что не влияет" именно питон часто становится ботлнеком, даже при запуске тяжёлых нейронок, вот так да.

Можно ли парсер логов сделать на расте? Да. Он будет быстрым? Да. Будет ли он столь же понятен изящен и лаконичен как на питоне или прости господи perl. Полагаю нет.

Думаю, вы сильно удивитесь, но Rust - это один из лучших языков для написания парсеров. Наличие нормальных енумов и довольно продвинутого pattern matching'а позволяет как очень легко описывать токены, так и легко разделять логику парсера под разные токены.

А на OCaml или Haskell еще лучше!

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

Я поэтому OCaml и привел в пример =)

Так а токсичность-то в чём? Простая же логика: язык A позволяет допустить класс ошибок N, а язык B - N-5 (условно). Если переписать программы с A на B, количество ошибок уменьшится. Меньше ошибок - лучше мир. Ну допускает автор такие вполне логичные (ему и мне и ещё некоторым людям) мысли, и что?

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

Но токсик именно автор, а не вы, ага

Серьёзно? Это и правда очень интересное утверждение. Приведите, пожалуйста, примеры конкретные в сравнении с аналогичными ситуациями в комьюнити других языков? Мне наоборот показалось там все такие няши, что аж не верится и Линуса на них нет)

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

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

Абсолютно токсичное по отношению к остальным, сами они этого не видят. Невероятно много контента от различных авторов и комментаторов с +- одним тезисов "А Rust лучше...". Язык может и не плохой и место для него найдется. Но ребята напоминают секту, а цель этой секты весь мир на Rust переписать. А для меня например, т.к я не пишу на rust, некоторые примеры из этой статьи абсолютно не читабельны, хоть и местами смахивает на кресты. Охотно верю, что проблема в отсутствии привычки и наметанного глаза к такому синтаксису. Но многие другие языки мне удавалось читать и понимать без знаний. Например Go отлично читается любым разработчиком знакомым с С подобным языком, почти любая конструкция будет очевидной.

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

Мне кажется, перебор с символами пунктуации в синтаксисе языка: .|&*::-><(),_>>()?;

Это в коде еще lifetime-ов не было, количество знаков пунктуации удвоилось бы.

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

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

От круглых скобок в if'е избавились - в отличие от многих языков программирования, они в Расте не обязательны (т.е. "if x > 0", а не "if ( x > 0 )" ).
От фигурных скобок можно было бы избавиться методом Питона.
От круглых скобок в функциях одного аргумента теоретически можно попробовать избавиться, но если аргументов более одного?

Мне очень интересно было бы увидеть подходы, за счёт которых можно было бы избавиться от лишних символов, одновременно увеличив выразительность кода!

Haskell =)

Так ведь он как раз один из лидеров по использованию "закорючек", только эти "закорючки" определяются в библиотеках.

Использование ключевых слов удлинило бы программу, сделав её набор медленнее

Я очень надеюсь, что вы с пользой потратили эти сэкономленные 100 миллисекунд?

Мне в этом смысле нравится Питон. Код читается почти как просто английский текст.

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

Появилось впечатление, что Rust сравнивался в 1ю очередь с Python. И после Python он оставил приятные впечатления.

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

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

P.S. Да, статью "что не так с Golang" я бы почитал.

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

Но это справедливо для небольших проектов.

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

К сожалению, у меня нет личного опыта подобных проектов в Rust. Мое мнение о Rust основано на опыте больших проектов в других языков, но о больших проектах говорят в Fast Development In Rust, Part One, Beyond Safety and Speed: How Rust Fuels Team Productivity, Grading on a Curve: How Rust can Facilitate New Contributors while Decreasing Vulnerabilities и нескольких реддит постах из начала статьи, так что мне кажется, что моя экстраполяция вполне валидна.

Лично я для большого проекта брал бы язык с более очевидным синтаксисом

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

Да, статью "что не так с Golang" я бы почитал.

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

Но уже много кто написал такие статьи, я рекомендую I want off Mr. Golang's Wild Ride, Lies we tell ourselves to keep using Golang, They're called Slices because they have Sharp Edges: Even More Go Pitfalls, Golang is not a good language.

Для примера добавлю сюда примеры кода, который вызывает у меня вопросы в духе "а хоть кто-то при разработке над чем-то кроме GC и async рантайма думал вообще?":

Заголовок спойлера
package main

import "fmt"

func main() {
    s := []int{2, 3, 5, 7, 11, 13}
    printSlice(s)

    // Slice the slice to give it zero length.
    s= s[:0]
    printSlice(s)

    // Extend its length.
    s = s[:6]
    printSlice(s)
    
    s= append(s, 7)
    printSlice(s)
    
    // Works just fine
    d := s[0:cap(s)][11]
    fmt.Printf("len=%d cap=%d d=%v\n", len(s), cap(s), d)
    
    // Obviously this panics
    g := s[11]
    fmt.Println(g)
}

func printSlice(s []int) {
    fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s[0:cap(s)])
}
package main

import (
	"fmt"
	"math/rand/v2"
)

type SlidingWindow struct {
	s      []int
	offset int
	size   int
}

func (w *SlidingWindow) next() []int {
	if w.offset > (len(w.s) - w.size + 1) {
		return nil
	}
	t := w.s[w.offset : w.offset+w.size]
	w.offset += 1
	return t
}

func sliding_window(s []int, offset int, size int) []int {
	return s[offset : offset+size]
}

func main() {
	s2 := []int{1, 2, 3, 4, 5, 6}
	if rand.IntN(10) > 5 {
		s2 = append(s2, 7) // panics without append
	}

	sw := SlidingWindow{s2, 0, 3}

	for w := sw.next(); w != nil; w = sw.next() {
		fmt.Println(w)
	}
}
package main

import (
	"fmt"
	"math/rand/v2"
)

func really_big_func() (a, b bool) {
	// a lot of code
	// Happy debugging, suckers 
	true := rand.IntN(10) > 5
	false := rand.IntN(10) > 3
	fmt.Println(true)
	fmt.Println(false)
	// a lot of code
	return true, false
}

func main() {
	fmt.Println(really_big_func())
}
package main

type Container struct {
	a     int              // old field
	Items map[string]int32 // new field
}

func (c *Container) Insert(key string, value int32) {
	c.Items[key] = value
}

func main() {
	c := Container{a: 2}
	c.Insert("number", 32)
}

Я наоборот слышал, что рефакторить сложно Rust. Особенно если где какой тип поменять надо (или его обертку). Но тут лично за себя отвечать не буду - лично не участвовал в больших рефакторингах c Rust (хотя и очень косвенно наблюдал, и по сложившемуся впечатлению - это было "больно").

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

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

Вопрос с большим проектом не в синтаксисе, а в читаемости кода - чуть ниже расписал. UB и Nothing - не такие уж и большие проблемы на практике, я бы сказал (уточню, что с C/C++ я очень мало работал).

"хм, кажется это какой-то бред" у меня возникал и при чтении Rust book. Но уже не вспомню толком на какие моменты.

Тип менять больно в любом языке, в котором есть типы. Rust тут, напротив, хорош возможностью заранее "подстелить соломку" в виде псевдонима для типа (type alias) - в таких языках как C# даже это невозможно (и ничего, как-то рефакторим код).

Честно скажу, этот аргумент именно что слышал в каком-то обсуждении, по существу, из своего опыта, не могу ни подтвердить, ни опровергнуть.

P.S. А рефакторить тип в языках, где нет типов - это еще веселее )))
P.P.S. Да, я знаю что "нет типов" говорить некорректно. Динамическая типизация не исключает типы.

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

У меня в моем трассировщике как раз есть такой пример: в оригинальной статье на C++ использовались как раз алиасы для color, vec3 и point3. Я не поддаваться на эту легкость и сделал три независимых типа (заодно узнал, как писать свой derive чтобы не копипастить базовые операции над типами).

Особенно если где какой тип поменять надо

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

Но это справедливо для небольших проектов. Для большого я бы его не брал.

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

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

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

Про сложность ревью кода в Beyond Safety and Speed: How Rust Fuels Team Productivity говорил Lars Bergstrom. По внутренним опросам в Google android, 50% опрошенных сказали, что код на Rust проще проверять (возможно, что это по сравнению с C++ и тогда это не то, чтобы сильно впечатляюще). Так что это дело просто привычки, вместо поиска скрытого UB можно сконцентрироваться на других вещах.

Почему мне не сложно читать код библиотек на Rust? Я профессионально программирую на Rust уже более 5 лет. Но даже в первый год его изучения я прочитал СТОЛЬКО чужого кода в библиотеках, сколько не читал за предыдущие 10 лет. Так что не спешите обобщать свои выводы на всех.

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

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

Мне кажется, борьба с компилятором – это хорошая возможность избежать проблем в проде. В основном, это ошибки про заимствования.

Я в основном пишу сейчас на Go, но с некоторыми вещами все еще не свыкся (внимание, дальше идет только очень субъективное ощущение):

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

  • Нет разделения mutable/unmutable указателей.

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

  • Лбв к скр им п.

  • Система импортов кажется топорной, не знаю как лучше объяснить.

  • Использование первого символа названия для определения публичности.

  • Обработка ошибок очень топорная и ограниченная.

Да, я тоже с Golang работаю после Rust (правда c Golang познакомился задолго до Rust). И со всеми пунктами согласен.

Но с опытом пришло понимание, что "чем проще - тем лучше". И Golang - это именно что простой язык. Он далеко не идеальный, но за счет простоты - я могу ему простить это.

P.S. сильно веселее было раньше, когда у Golang не было еще и менеджера зависимостей )

Согласен, когда принимаешь "проще - лучше" и заточенность под определенную нишу, то становится жить веселее)

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

А как это проявляется?

Это все субъективно, может быть в чем-то ошибаюсь, но вот что не очень нравится и кажется топорным:

  1. Между файлами одного пакета все испортируется в неявном виде из разных файлов

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

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

  4. Нет реэкспорта

  1. Вполне стандартная тема для многих языков. Тот же C# шарит все внутри одного неймспейса между файлами. В Go есть одна вещь, которая сильно улучшает читабельность - можно однозначно отличить, откуда символ пришел. Если у него нет идентификатора пакет, то это локальный символ из текущего пакета (опускаем dot-imports, которыми никто не пользуется). Иначе, всегда будет идентификатор пакета и по импортам ясно, откуда он пришел. Теже шарпы или Java читать намного сложнее, потому что при импорте все символы, как правило, доступны напрямую без необходимости писать идентификатор неймспейса. И хер ты без IDE поймешь, откуда взялся тот или иной класс.

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

  2. А зачем это нужно? Ты либо импортируешь пакет, либо нет. К пакету всегда обращение через его имя, поэтому пофиг, сколько десятков тысяч символов в нем. И читается это всегда однозначно

А зачем это нужно? Ты либо импортируешь пакет, либо нет.

Для разрешения конфликтов имён. И уменьшения числа потенциальных конфликтов.

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

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

Возникающих при dot-imports, которыми никто не пользуется.

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

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

Согласен. Это тоже немного раздражает в идеалогии Go, что если в языке чего-то нет, то это не означает ограничение языка, а это означает что разработчику это не нужно.

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

У вас больше вопрос к разработке и опыту с ide, которые подерживают rust все лучше с каждым годом, но все еще не очень по сравнению с Java/Kotlin/C#. JetBrains уже отдельную ide для rust выкатили RustRover но еще в альфа релизе. А rust компилятор выводит довольно хорошие подсказки для новичков. Тем не менее после полугода работы с языком понимаешь сам без помощи компилятора, когда код собирется и как писать нужно т.е становишься продуктивнее и в плане чтения кода тоже. Библиотеки конечно бывают разные, ваша правда) Но потенциал развития довольно большой. Все частоиспользуемые либы вроде сериализаций(serde), веб фреймворков уже довольно хороши. Касательно больших проектов. Большая часть бэкенд сервисов одной крупной криптобиржи написано на rust https://blog.kraken.com/product/engineering/oxidizing-kraken-improving-kraken-infrastructure-using-rust и воочию выглядели неплохо.

Не знаю на счет RustRover, но раньше их плагин Rust основывался на LSP. И с момента прихода rust-analyzer взамен RLS стало намного лучше.

Но большой разницы с той же VS Code я не увидел - LSP и там и там.

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

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

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

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

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

Правда размер бинарников (debug), объем артефактов сборки (десятки и стони гигов), длительность компиляции (я понял, зачем мне 16 ядер и 64Gb RAM) - могут сыграть не в пользу Rust и тут ))) Но это больше вопрос техники.

раньше их плагин Rust основывался на LSP.

У них уже довольно давно своя реализация, не rust-analyzer.

Возможно.

Я скорее основываюсь на впечатлениях, а не на твердом знании "что там под капотом".
Во времена VS Code с RLS существенной разницы c Rust-плагином я не увидел (возможно совсем небольшое преимущество было за JetBrains - деталей не помню).
Во времена VS Code с rust-analyzer существенной разницы c Rust-плагином я не увидел.

В переходный период Rust-плагин JetBrains отставал, как отставал RLS от rust-analyzer.
Но я все возможности досконально не сравнивал - только подсветку типов, да авто-дополнение (то, с чем работаешь больше всего и что можно быстро сравнить). В идеале, конечно, надо было погонять IDE "и в хвост, и в гриву", для формирования более целостного впечатления.

И да, это было достаточно давно - как там сейчас обстоят дела я не скажу.

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

В плане "железа" для rust проектов экономить не приходится. Ведь если с нуля, то компилируются все все зависимости.

"Большой монолит" когда-то был достаточно безальтернативной практикой )
Микросервисный подход получил популярность "всего лишь" лет 10 назад... Лишь с появлением и развитием Docker он стал популярен - когда управлять кучей сервисов стало сильно проще.

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

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

P.S. а еще есть очень холиварный вопрос - когда микросервис перестает быть микро?

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

Согласен, согласен. Тут общего мнения нет. It depends.

В плане веба большой монолит чаще (но не всегда) считается плохой практикой.

Что бы холивары не разводить, поэтому так и выразился) "Маленький/средний монолит" точно считаю нормой.

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

А это вечный вопрос)

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

Когда он становится монолитом?

Если вам действительно интересно и вы не хотите просто потролить.

То есть к примеру классификация Amazon где они делят все существующие решения на monolithic, SOA (сервисы) и microservices(микросервисы) и описывают где проходит грань с примерами. Естествено я допускаю что классификаций много и мнения есть другие. По этому это вечный вопрос)

Я просто пошутил. А смайлики тут непопулярны. Тут все такие серьезные... Я знаю и что такое микросервисы и [классификации] и имею 35+ лет опыта в IT. Но спасибо что потратили время на меня. Я это ценю. (не сарказм)

А вот насчёт безальтернативности - не факт. К примеру, в той же WS-Addressing такая штука как Reference Parameters идеально подходит для передачи контекста запроса между микросервисами. Не могли же её придумать с потолка? Значит, нечто похожее на микросервисы писалось, пусть и называлось по-другому.

Был SOAP, были другие методы и подходы к удаленному вызова процедур (и удаленного выполнения кода в целом).
Сам по себе подход "вызов сервиса из другого сервиса" был известен намного раньше появления термина "микросервисы" и популяризации такой архитектуры.

Для простого RPC достаточно заголовков MessageId и RelatesTo (ну и Action, хотя некоторые библиотеки обходятся без него).

Reference Parameters нужны для хореографии распределённых саг.

Могу добавить, что в CLion/RustRover была проблема с macro expansion (на примере библиотеки RTIC). Как сейчас в Nova/RustRover не тестил, подозреваю что проблемы с автокомплитом остались, в VSCode это всё из коробки работало.

Безотносительно Раста, такие статьи, подсознательно относишь как к рекламе Гербалайфа. Слишком хорошо, что бы быть правдой. Автор, конечно же не виноват, но...