Pull to refresh

Comments 71

Уже был один язык, куда добавили абсолютно всё - PL/I. И что-то он сейчас не очень популярен

Важно не то, сколько добавить, важно то — как это сделать. Т.е. не количество фич само по себе, а внутренняя взаимогармоничность разных элементов языка. Этого я у многих современных языков не вижу.
Ну а С∀ — просто интересный образец того, как люди видят недостатки в существующем и пытаются их как-то исправить. Здесь интересно само направление мысли, что-ли… Вот например ссылки. На Хабре уже была статья о недостатках ссылок, с предложением о том как эти недостатки устранить. Здесь — другой подход (тоже не лишенный недостатков!). Но интересно сравнить, обдумать…

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

Но вот это

void ?{}( CntParens & cpns ) with( cpns ) { status = Cont; }

- это взрыв на скобочной фабрике.

forall(otype T | { T ?+?(T,T); }

А вот это уже начинает напоминать регулярки.

А также перегрузка ключевых слов, потоки зачем-то в виде builtin типа.

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

Синтаксис объявления переменных мне нравится( т.е. консистентный и фиксированный порядок появления const и т.д. Но раз уж совместимость с C всё равно сломана, то зачем тащить за собой остальные странности.

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

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

Разве что генераторы забыли.

const * [ 5 ] const int y;

А мне нравится. Всё остальное "ну так", а это прям нравится.

Мне кажется, сейчас новые языки взлетают только если они способны предложить или большую экосистему, включающую старые библиотеки, или если за языком стоит корпорация, продвигающая язык в своих продуктов, а поэтому пилящяя для него ту же самую экосистему, обвязки, компиляторы, чекеры и т.д.
Либо язык действительно решает КРАТНО лучше задачи, чем его существующие альтернативы.
Для плюсов альтернатива нужна не только в языке, но скорее в нормальном менеджере пакетов, например, и прозрачной системе сборки.

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

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

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

Совершенно идиотское сочетание двух правил:

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

  2. Если при этом, во время исполненения, программа приводит к неопределённому поведению (в том числе не существовавшему на момент написания оной программы!) — то программу можно ломать.

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

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

Практически таковых есть два: Swift (у Apple) и Rust (для всех остальных).

Как с этим у С∀ — но, подозреваю, что ещё хуже, чем у C и C++.

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

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

О том как и почему появилось это понятие я сам когда давно писал.

Когда неопределённое поведение появилось другой альтернативы этому всему, в общем, не было.

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

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

Более того: как сейчас выясняется ещё в 2004м году разработчики компиляторов выбили себе карт-бланш на UB, которые они до сих пор не могут описать!

Вдумайтесь в уровень этого идиотизма: правила, нарушение которых позволяет вашему компилятору сломать, к чёртовой матери, вашу программу до сих пор не описаны в стандарте потому что их никак не могут разработать (ибо там противоречия воникают), ни в одном стандарте их нет (одна из последних попыток их добавить), но если вы их нарушаете — то получаете проблемы. Класс, да? Как таким языком вообще можно пользоваться? Это ж минное поле получается!

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

Фишка же в том, что ровно это и предполагалось делать с C. Это было даже явно прописано в соответствующем документе: Undefined behavior gives the implementor license not to catch certain program errors that are difficult to diagnose. It also identifies areas of possible conforming language extension: the implementor may augment the language by providing a definition of the officially undefined behavior.

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

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

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

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

Раньше были архитектуры с 9-битными байтами, и не с комплементарным представлением отрицательных чисел.

Дык фишка вот в чём: сейчас таких архитектур нету и современные версии C++ их не поддерживают, однако компиляторам по прежнему разрешено ломать программы, которые пользуются тем, что в современных процессорах комплемендарное представление отрицательных чисел не используется!

Неопределённое поведение — это всё то, чьё поведение не было определено.
Поэтому пожелания «описать все случаи неопределённого поведения» абсурдны — примерно как попытка описать число через его неописуемость.

Другими словами: это не какие-то особые запрещённые случаи, прихотливо разбросанные по пространству допустимых программ. Наоборот: стандарт определяет поведение довольно узкого подмножества всех синтаксически корректных программ. Всё то, что не определено — неопределённо.

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

Поэтому пожелания «описать все случаи неопределённого поведения» абсурдны — примерно как попытка описать число через его неописуемость.

Это что за идиотизм? Открываете любой стандарт C (не C++!), смотрите в приложение J. Там все случаи описаны.

В C++ они не сгруппированы в одном месте, а разбросаны по тексту, но тоже все описаны.

Неопределённое поведение не означает, что ситуация, в которой оно возникает не определено. Вот последствия — да, не определены. А действия, совершаемые программой для того, чтобы мы могли заявить “да, у нас-таки случилось UB” все описаны, в противном случае мы не могли бы вообще, в принципе, определить — приводит программа к UB или нет, определено её поведение или может быть любым.

Другими словами: это не какие-то особые запрещённые случаи, прихотливо разбросанные по пространству допустимых программ.

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

Пример: разименование нулевого указателя. В разных системах это может привести к разным последствиям (скажем в MS DOS вы обратитесь к адресу обработчика нулевого перерывания и можете его испортить, а в Linux или Windows x64 система отдиагностирует ошибку и вашу программу остановит… хотя ошибку можно перехватить и обработать).

Я не знаю вообще ни одной системы, где бы это поведение не было бы описано! Да, не в стандарте C или C++, а в стандарте POSIX или в даташите на SOC… но оно таки описано!

Но разработчикам копиляторов пофиг: раз стандарт C++ говорит, что это неопределённое поведение — значит у нас карт-бланш, будем крушить всё, до чего сможем дотянуться.

Или другая “типа оптимизация”, превращающая i || !i не в true (что было бы понятно и логично), но в false. Это не так-то просто было сделать, закон исключённого третьего, вроде бы, должен бы подобные вещи отсекать. Но… напряглись! Смогли! Ура, товарищи, теперь программисты получили ещё один способ выстрелить себе в ногу!

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

Врёте. Вот в том самом примере с realloc, вокруг которого столько копий ломали никто так и не придумал как можно найти UB, интерпретируя программу с точки зрения C89. Ибо “гениальная” интепретация “когда realloc вернул тот же указатель, что и до вызова realloc, то это всё равно другой объект” можно вычитать из C99 и последующих стандартов, но не из C89.

Да и то — их нужно невероятно “креативно” читать, чтобы в этой программу UB углядеть. Нормальному программисту никогда в жизни не придёт в голову идея, что вот такой вот код: p = realloc(q, …); делает q, внезапно, указывающим на место в памяти один-после-конца массива (нулевого размера, видимо)! А только такая, “креативная”, интерпретация позволяет заявить, что в этой программе имеется UB.

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

Ни то, ни другое, увы. Окройте же, наконец, пропозал, почитайте и ужаснитесь: там целая куча примеров того, как разные компиляторы ломают валидные программы — с предложением вот всё вот то безобразие, которое компиляторы вытворяют объявить валидным, а стандарт изменить, чтобы эти прораммы перестали быть валидными программами на C++.

Примерно как это уже произошло с realloc ом. А потому что в 2004м году было решение комитета по стандартизации, в стандарт ничего протащить не удалось (а ведь 17 лет прошло!), но компиляторы уже считают, что некоторые “плохие” (но не нарушающие стандарт, заметим!) программы можно ломать. И ломают. Красота?

В примере с q = realloc(p, …) вы не имеете права использовать p после успешного вызова, т.е. когда q != NULL: ни разыменовывать, ни сравнивать. Цитата из C89: The pointer returned if the allocation succeeds is suitably aligned so that it may be assigned to a pointer to any type of object and then used to access such an object in the space allocated (until the space is explicitly freed or reallocated). <...> The value of a pointer that refers to freed space is indeterminate. Не вижу, о чём тут ломать копья: сама попытка определить, тот же указатель возвращён или другой — уже UB. Освобождённый указатель не «указывает на место в памяти один-после-конца массива», а равнозначен неинициализированному.

Ну в процитированном абзаце, всё на самом деле упирается в то, как понять reallocated. Я вижу два способа: 1) фактическая реаллокация, т.е. память была перемещена на новое место, 2) тупо факт вызова функции realloc. И UB тут можно натянуть только при второй трактовке, что ну очень натянуто. А если учесть ещё, что в первом подчёркнутом предложении речь идёт именно о памяти (in the space allocated), а не о значении указателя, то первая трактовка кажется единственно верной.

В примере с q = realloc(p, …) вы не имеете права использовать p после успешного вызова, т.е. когда q != NULL: ни разыменовывать, ни сравнивать.

С какого такого перепугу?

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

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

until the space is explicitly freed or reallocated

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

А вот и про испольование самого значения указателя:

The value of a pointer that refers to freed space is indeterminate

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

И кстати, то же самое даже в C18. Там это правило вообще в другом месте, но звучит оно так: The behavior is undefined in the following circumstances: … the value of a pointer that refers to space deallocated by a call to the free or realloc function is used.

Обратите внимание: тут снова идет не речь об указателе на исчезнувший объект, а об указателе, указывающем на освобождённую память. А вот сравнить два указателя (чтобы понять, указывают один из них на освобождённую памяти или нет) нам разрешили явно: The realloc function returns a pointer to the new object (which may have the same value as a pointer to the old object), or a null pointer if the new object has not been allocated.

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

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

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

Two pointers compare equal if and only if both are null pointers, both are pointers to the same object (including a pointer to an object and a subobject at its beginning) or function,both are pointers to one past the last element of the same array object, or one is a pointer to one past the end of one array object and the other is a pointer to the start of a different array object that happens to immediately follow the first array object in the address space.

Тут у нас появился очень-очень интересный момент, которого в C89 не было. Там правило было другое:

If two pointers to object or incomplete types compare equal. they both are null pointers. or both point to the same object. or both point one past the last element of the same array object.

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

А в C99, да, поскольку обращаться к объекту, который был уничтожен нельзя (этого момента в C89 тоже нет, как и уничтожения и создания нового объекта в realloc), и у нас резрешён невозможный в C89 случай, когда два указателя равны, но один из них валиден, а другой невалиден, то можно попробовать сову на глобус натянуть и объяснить, что, поскольку объект-таки удалён, а на его месте теперь новый, то даже если два указателя равны, то одним можно пользоваться, а другим нельзя. Ну, типа объект схлопнулся, память мы не освободили, но объект уничтожили, потому это — теперь вот такой вот one past the end of one array object указатель.

Мне эта интерпретация кажется несколько, как бы это сказать, безумной, но ещё раз повторяю: в C89 она вообще невозможна. Так как там не бывает такого, что указатели равны, но один валиден, а другой — нет. И объекты в realloc не удаляются и не создаются. А всего лишь перемещаются: The realloс function changes the size of the object pointed to by ptr to the size specified by size. и The realloс function returns either a null pointer or a pointer to the possibly moved allocated space.

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

Максимум, чего удалось добиться — упоминания того, что C89 это очень старый стандарт (а ничего что ещё пару лет назад один очень распространённый компилятор ничего новее не поддерживал?) и сегодня не актуален, а для C99, видите, мы какую шнягу красивую придумали.

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

С другой стороны, волосы на голове начинают шевелиться от того, что пишу я себе под Windows x64, никого не трогаю, скастил указатель в uint64_t для каких-то своих целей, а потом обратно в char*, и мне компилятор в полном своем праве ради шутки вставил Format C: (а нечего UB делать на ровном месте).

Да не держат они в уме всякие Эльбрусы. Забудьте.

Оптимизация, которая там делается (превратить *q в 1, а *p в 2) в некотором смысле напрашивается. Но она опасна. Валидна только тогда, когда можно доказать, что *p и *q укаывают в разные места памяти.

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

Возражения некоего частного лица привели к тому, что эту затею, в тот раз отставили.

В C99 к этой затее вернулись и добавили в язык __restrictсо слегка другой семантикой.

Однако это тоже, особо не сработало: люди редко исползуют __restrict и ещё реже — правильно.

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

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

Начавшаяся после этого борьба брони и снаряда — почти привела к катастрофе: оказалось, что люди, в общем, когда программы пишут решают прикладные задачи и тесты гоняют, а не ребусы на тему “а нет ли у нас тут UB? а если покреативней поинтерпретировать — всё равно нет? а если воображение напрячь?”.

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

Но настоящая катастрофа произошла тогда, когда разработчикам компиляторов оказалось и этого мало, они выбили себе разрешение прописать себе в стандарт дополнительные UB (читаем внимательно: it would be desirable for the Standard to mean это ни разу не mean, это всего лишь позволение изменить стандарт, не признание того, что стандарт что-то там кому-то позволяет), но не сделав этого, начали этим фиговым листком прикрываться.

В результате мы получили язык для которого не существует ни одного современного компилятора! Да, вот так, просто: ни одного!

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

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

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

Проблема в том, что долгое время никто не мог придумать: что же, всё-таки, с этим делать. Понятно, что хочется как-то сделать так, чтобы в языке UB не было совсем (во многих других языках их нет: C#, Java, JavaScript и так далее), но было непонятно как удалить UB из языка без GC: у вас же, в таком языке, можно удалить объект, на который, где-то там ещё, есть “живые ссылки”! А можно ведь, ко всему прочему, из разных потоков, параллельно, память менять, там тоже разные процессоры много разного интересного может насчитать.

Решение Swift было промежуточным, почти как у языков с GC: а давайте мы сделаем малоинвазивный GC на счётчиках, тяжёлый рантайм ему не нужен, но безопасность можно обеспечить. А все опасные конструкции вынесем в стандартную бибиотеку.

Такой себе недо-managed язык. Если компилятор сможет, в некоторых местах, манипуляции со счётчиками извести — он может даже достаточно быстрым оказаться. Вроде как проблемы скорости всё равно не решились, впрочем (Apple, по итогу, сделала аппаратное ускорение в свои процессорах, но такое могут себе позволить себе не все).

А вот Rust сделал куда более интересную вещь: предложил отдельно пометить все места в программе, где ипользуются конструкции, которые могут вести к UB! И оказалось, что, при должном упорстве, можно сделать так, чтобы в большой программе так было пемечено где-то 1% года!

А в остальной программе пусть UB не будет (утечка памяти — это не UB).

Это было смелое решение, в которое, если честно, я много лет не очень верил. Смотрел на то, как они барахтаются, но не верил, что они могут сделать язык, близкий по выразительности и скорости к C++, но без UB (ну Ok, “почти без” UB, “минное поле” в 1% кода это куда лучше, чем “минное поле” в 100% кода).

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

Оперевшись на весьма интересное наблюдение: программисты на “современном C++” и без того очень часто используют std::unique_ptr и часто действуют в парадигме блокировки-чтения записи, когда у вас в программе есть, у каждого объекта, либо один писатель, либо много читателей (но писателей тогда нет ни одного, XOR а не OR).

Это позволяет решить проблему с UB, но некоторые конструкции оказываются невозможными (например банальную очередь создать уже нельзя).

С другой стороны — так ли часто вы пишите неблокирующие стеки и очереди? Я наблюдал как-то за этим увлекательным процессом. Там на примерно 100 строк кода ушло две недели написания, а количество строк с описанием было примерно на порядок больше, чем кода. И это после того, как описание сильно упорядочили и порезали (добавив ссылок на несколько статей).

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

Если хочется обсудить проблемы с UB, то приводить realloc в пример — так себе идея. Уже и не помню, когда в последний раз им пользовался.

Как я понял из вашего текста, вот это — UB
void* q = realloc(p, newsize);
if (p == q) { ... }
А вот это — корректно
intptr_t p1 = static_cast<intptr_t>(p);
void* q = realloc(p, newsize);
if (p1 == static_cast<intptr_t>(q)) { ... }
Странно конечно, но не смертельно.

Мне вообще мало интересны особенности стандартной библиотеки. Чаще пользуюсь или своими обёртками, или фреймворками, или функциями ОС.

Если хочется обсудить проблемы с UB, то приводить realloc в пример — так себе идея.

А почему, собственно?

Уже и не помню, когда в последний раз им пользовался.

А какая разница? Нам же важно понять: имеет ли смысл та логика, которой руководствуются компиляторы или нет.

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

А вот это — корректно:

intptr_t p1 = static_cast<intptr_t>(p);
void* q = realloc(p, newsize);
if (p1 == static_cast<intptr_t>(q)) { ... }

Ух ты, прелесть какая. А вот совершенно неясно - оно корректно или нет. Нет, clang считает, что так тоже нельзя. Хотя тут мы вообще не используем ужасный, кошмарный, “отравленный” указатель и сравниваем просто числа.

Собственено ровно в эту проблему и упёрлись разработчики Rust: выяснилось, что правила, которыми руководствуется компилятор внутренне противоречивы и их необдуманное применение может ломать даже самые простейшие программы без всяких realloc'ов и прочей мути.

Ровно по этой же причине, кстати, не удаётся и добавить pointer provenance в стандарт с 2004го года. Просто никому не удаётся придумать такие правила, которые бы, с одной стороны, позволяли разработчикам компиляторов производить все те оптимизации (ну или большинство, хотя бы) , которые они напридумывали, а с другой — не приводили бы к тому, что масса ну совершенно очевидно правильных программ не оказались бы поставлены “вне закона”.

Чаще пользуюсь или своими обёртками, или фреймворками, или функциями ОС.

О! У меня для вас хорошая новость! В соотвествии с “креативным” прочтением стандарта у вас вообще не может быть такой функции, как mmapили munmap. Совсем. Никогда.

Потому, собственно, описание malloc/calloc/realloc/free и оказалось частью стандарта, что их написать на стандартном C в принципе нельзя.

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

Они могут это делать потому, что являются частью стандарта и, соотвественно, могут делать то, что никакие другие функции делать не могут.

А никакие другие способы, не сводящиеся к этой четвёрке, невозможны и, соотвественно, функции map/unmap (и их аналоги в Windows) — вызывать, в корректной, программе, нельзя.

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

Вы счастливы?

Поигрался с компилятором. Видимо, всё это происходит от того, что с malloc/free есть очень агрессивные оптимизации. Если я указатель не возвращаю из функции, компилятор может вообще не вызывать никакие библиотеки, а разместить значение на стеке или даже в регистрах. И если я «забуду» сделать free, даже утечки памяти не будет.

Странность с realloc видимо нужна, чтобы в глубинах компилятора моделировать его как malloc+memcpy+free. А malloc всегда возвращает новые адреса, ни с кем не пересекающиеся.

Потому, собственно, описание malloc/calloc/realloc/free и оказалось частью стандарта, что их написать на стандартном C в принципе нельзя
Почему нет?
char heap[100500*1024];
— и откусывай оттуда своими аллокаторами.

Соглашусь, приведённые вами примеры очень континтуитивны и генерируют очень много wtf на строку кода. Но в реальной жизни никто не захочет сравнить указатель с чем-то после free/realloc. Опять же, это только проблема free/realloc, а реально пользуются API/фреймворками, в C++ вообще этого нет (нет realloc в парадигме new/delete — нет проблемы).

Почему нет?

Потому что C - это не C++.

char heap[100500*1024];
— и откусывай оттуда своими аллокаторами.

В C++20 это сработает благодаря PR593. Только не забудьте правильные конструкторы/деструкторы вызывать. Конечно вы получите, таким образом не malloc/free, а new/delete, но для C++ этого хватает.

А вот в C (и более ранних версиях C++) — не получится, потому что нет никакого способа превратить кусок этого массива в объект другого типа и, главное, нельзя заставить этот объект перестать существовать.

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

Опять же, это только проблема free/realloc, а реально пользуются API/фреймворками, в C++ вообще этого нет (нет realloc в парадигме new/delete — нет проблемы).

Конкретно этой проблемы нет, зато есть масса других. Одно существование std::launder — много говорит о масштабах разрухи.

Как я уже сказал: проблема не в том, что “правила игры”, навёрнутой вокруг этого всего, сложно соблюсти, проблема в том, что никто не может их даже внятно сформулировать!

Известно только, что то, что прописано в стандарте — это “не то”, то есть соблюдения стандарта при написании программ — недостаточно.

P.S. С точки зрения нормальных людей было бы разумно, понятно, трактовать все эти эффекты консервативно, разрешая оптимизации в отдельных местах, помеченных как-нибудь особо. Но тогда упадёт скорость на бенчмарках, а этого разработчики компиляторов, конечно, допустить не могут. Но чёрт бы с ними, дали бы ключик -fno-provenance (как уже дали -fno-strict-aliasing)… но нет, не хотят. Слишком сложно, говорят. А избегать-того-не-знаю-чего — раз плюнуть, да?

Ух ты, прелесть какая. А вот совершенно неясно — оно корректно или нет. Нет, clang считает, что так тоже нельзя.

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


В общем, проблемы о того, что разработчики стандарта тихо-мирно поменяли смысл сравнения указателей с побитовой эквивалентности на логическую эквивалентность и под капотом сравнивают не все биты, чтобы заявить, что указатели одинаковые. Вот и получается, что 0b10011 равно 0b00011. Как я понимаю, в стандарте явно этой логики не прописано, а все какими-то окольными путями и полунамеками.

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

Если бы всё было так просто. Тут не в битах дело. Почитайте пропозал-то: они статически, во время компиляции, пытаются доказать, что некоторые указатели указывают на разные объекты в памяти.

И вот тут у них у самих неразбериха: в соотвествии с “креативным” прочтением правил указатель p протух, ладно, но p1 — это ж число, оно, вроде как, протухать не должно… или должно?

Вот если оба указателя превратить в числа, а потом обратно — clang, наконец-то, перестаёт издеваться над здравым смыслом. Но это законно или нет? Почитайте к чему все эти попытки ведут.

Правил нет, но вы держитесь. В смысле — их соблюдайте.

Вот если оба указателя превратить в числа, а потом обратно — clang, наконец-то, перестаёт издеваться над здравым смыслом
У меня не перестаёт, наблюдаю вывод: 1 2

Да, вы правы, у меня, похоже, сбой godbolt был.

А никакие другие способы, не сводящиеся к этой четвёрке, невозможны и, соотвественно, функции map/unmap (и их аналоги в Windows) — вызывать, в корректной, программе, нельзя.

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


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

А представьте, что мы свою программу не динамически линкуем с HeapAlloc/HeapFree, а пробуем написать целую ОС и приложения в ней одним большим блобом. Так, что компилятор на момент компиляции всё про всех знает и может оптимизировать, как посчитает нужным. И вот тут можно очень хорошо отгрести.

Это, кстати, типичная ситуация для “малого” embedded, где собирается не программа, а прошивка.

И где компилятор, действительно, при использовании LTO знает всё про всю программу.

Вообще-то можно, и именно потому, что компилятор про эти функции ничего не знает.

Это не определение корректной программы, извините. Корректная программа не должна зависеть от знания компилятором чего-либо и создавать объекты иначе как через malloc/calloc/realloc и удалять иначе как через realloc/free.

В C++ ещё new/delete появляются.

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

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

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

Так они же не "равны", они "compare equal", если я правильно понял. То есть верно `p == q`. А то, что их можно при этом одинаково использовать, вроде нигде не написано?

А то, что их можно при этом одинаково использовать, вроде нигде не написано?

Написано. В C89 написано вот это

If two pointers to object or incomplete types compare equal. they both are null pointers. or both point to the same object. or both point one past the last element of the same array object.

Четвёртый вариант (и связанное с ним безумие, когда у вас есть два равных указателя, но один можно использовать, а другой нельзя) появляется только в C99.

Об этом, мне, собственно, заявили прямо:

If you believe pointers are mere addresses, you are not writing C++; you are writing the C of K&R, which is a dead language. The address space is not a simple linear space: it is a time-varying disjoint sum of many small affine spaces, which the compiler maps to a linear address space (for common target triples!). This has been in the standards since C99 (although perhaps not very clear for those not versed in standardsese).

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

Рекомендация была такой:

Viewing C/C++ as static is a mistake. Viewing it as a portable assembler is a mistake. Viewing it as "GCC behaved this way last I checked, so it still holds, right?" is also a common mistake. It is important to stay up to date on the standards, be aware of what fussy code looks like (or, practically, what not-fussy code looks like), and know how to ask for help, as I have stressed in each of my previous replies.

Офигительный подход как для языка основное достоинство которого — тот факт, что на нём написаны миллиарды строк кода, не так ли?

Теоретически я ешё как-то могу применить этот подход, благо у нас есть и разработчики clang в компании и весь код регулярно прогоняется через новые его версии (те, которые ещё в разработке), но, блин, они реально хотят сказть, что все разработчики, использующие C/C++ должны подобным заниматься? Опираться даже не на стандарт, а на what fussy code looks like (or, practically, what not-fussy code looks like).

Да они оху…, нет, охр… да блин, как на это без мата-то реагировать?

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

При этом от эпох комитет пренебрежительно отмахивается.


Swift (у Apple)

ЕМНИП там везде принудительный подсчёт ссылок. Любой системщик мгновенно поднимет на вилы. Плюс за пределами экосистемы эппла распространение стремится к нулю.

Swift — тоже развивается в сторону инопланетчнского синтаксиса.

При этом от эпох комитет пренебрежительно отмахивается.

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

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

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

Что? С покупкой машины времени затык? Не наши проблемы.

ЕМНИП там везде принудительный подсчёт ссылок. Любой системщик мгновенно поднимет на вилы. Плюс за пределами экосистемы эппла распространение стремится к нулю.

Со счётчиком ссылок компилятор неплохо борется. Со временем будет лучше. C++ так же развивался. Но то, что никто, кроме Apple, Swift не поддерживает — проблема, да.

Google немного поигрался, но забил, в конце концов.

Но это древняя традиция, ещё с прошлого века. Когда все переходили с C на C++ у Apple тоже был свой язычок — Objective C. Многие решения в Swift вызваеы необходимостью сосуществования с модулями на Objective C, а вне экосистемы Apple это никому не нужно, так что так и получается: у Apple — Swift, у всех остальных — Rust.

Практически таковых есть два

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

Языки, способные заменить C/C++ хотя бы в теории.

Это довольно много ограничений: например C/C++ популярны в embedded, где невозможно использовать большую стандартную библиотеку или, тем более, GC.

Также, разумеется, туда не лезет JIT, а интерпретатор не годится из-за потребления ресурсов.

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

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

Долгое время сколько-нибудь популярных альтернатив для C/C++ просто не было (почему, собственно, разработчики компиляторов и могли вытворять то безобразие, которое они вытворяли: типа, всё равно ж будете жрать кактус, куда вы денетесь?).

Сейчас они появились, но их, мягко скажем, немного.

P.S. Строго говоря Rust и Swift не являются единственной альтернативой. Есть ещё D, Zig и внушительное количество ещё более экзотических языков. Но в случае с D такая возможность остаётся чистой теорий (теоретически без GC можно обойтись, практически же вы, в этом случае, не сможете использовать почти ничего из уже написанного кода), а больше в первой сотне языков на TIOBE я таких языков не вижу (хотя может чего и упустил, там есть много языков, о которых я мало чего знаю).

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

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

Пока неясно — будет ли он развиваться или повторит судьбу бесконечных диалектов Nim, Oberon и прочей экзотики.

Rust сегодня поддерживают Amazon, Facebook, Google и Microsoft — то есть практически все крупные игроки, кроме Apple (у которых свой Swift есть и который они, конечно, не будут на Rust менять).

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

А для успеха языка поддержка его “большими” игроками очень много значит.

Потому практически, как я сказал, выбор между Rust и Swift сегодня. Завтра (образно: лет через 10, на практике-то) ситуация может и измениться.

А что насчет Zig?

У него, как и у C, идеология "Программист Знает Что Делает". Для меня это крайне существенный минус: как показывает практика, доверять мешкам с мясом нельзя.

Это довольно много ограничений: например C/C++ популярны в embedded, где невозможно использовать большую стандартную библиотеку или, тем более, GC.

Уже бодро Java прописался в embedded, как появились мелкие устройства помощней сразу стал нормой.

Уж так прям и нормой? А ничего, что Raspberry PI выпускается тиражом меньше десяти миллионов штук в год, а рынок микроконтроллеров - это больше десяти миллиардов штук в год?

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

Вы бы лучше про MicroPython вспомнили. У него требования полегче, можно где-нибудь на 5-10% встраиваемых систем запустить.

Но как только мы выходим за рамки штучных поделок — так сразу смысл его использования теряется. Так как платформы, способные запустить что-то на MicroPython всё равно в разы дороже, чем те, которые поддерживают C/C++/Rust.

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

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

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

Серьезные проекты, прошивка роутера какого-нибудь уже на Java.

А пример можете привести? Потому что максимум чего я видел — Java-applet для управления и настроек. Но он на самом роутере не исполняется, он в браузере на компьютере работает.
А так-то можно много чего записать в малинку, даже Windows, как известно, бывает и даже не только IoT. Но это тоже всё DIY.

А насчёт нормальных проектов… насколько оно на практике востребовано? Какая-нибудь статистика, подтверждающая ваши утверждения, есть?

Ну и, понятно, есть ещё такая штука как размытие самого понятия Embedded. Формально же компьютер Tesla, на котором Cyberpunk 2077 бегать может - тоже embedded, но там кроме этого компьютера ещё десяток других, попроще.

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

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

Но где-то, конечно, ставят мелкие МК с десятками байт оперативной памяти и этого достаточно.

И тут тоже интересно: какой процент вот этого всего? Потому что есть ощущение, что цена 32-битных микроконтроллеров снизилась настолько, что 8-битные или там 4-битные больше смысла не имеют (в какой-то момент цена всё равно упирается в корпусировку и монтаж, так что ниже какой-то цены у вас никакой контроллер опуститься не может).

Я знаю только про прикольное сравнение микроконтроллеров из USB-зарядок с компьютером Apollo, но там, всё-таки, не самые дешёвые контроллеры пользуют.

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

Я знаю только про прикольное сравнение микроконтроллеров из USB-зарядок с компьютером Apollo, но там, всё-таки, не самые дешёвые контроллеры пользуют.

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

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

ИМХО этого не произойдёт никогда. Слишком много разных систем сборки и пакетных менеджеров с разными идеологией и подходами к решению тех или иных проблем. Комитет, даже если бы захотел какой-то унификации, мгновенно потонул бы в перетягивании одеяла в десятке разных направлений.

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

Ему нужно одновременно быть достаточно абстрактным и достаточно близким к железу.

Аппаратные платформы постоянно совершенствуются и обрастают различными наборами инструкций, которые призваны ускорить работу программ. Но компиляторы (GCC, Clang) по-прежнему плохо справляются с автовекторизацией и не используют даже половины новых возможностей. Также в стандартных библиотеках нет многих битовых манипуляций (rotl и rotr появились только в С++20). Поэтому приходится писать код на интринсиках отдельно для каждой платформы. А в каком-нибудь ЯВУ есть универсальная кроссплатформенная библиотека всех возможных векторных и битовых операций?

Многие алгоритмы поддаются оптимизации, если известен диапазон значений переменных. Например, если для вычисления значений цвета использовался массив uint32 и конечные значения лежат в диапазоне [0; 255], то их можно обрабатывать как uint8. Но компилятор этого не знает, пока мы сами ему не сообщим или не реализуем упаковку байт вручную. В каком-нибудь языке есть аннотации диапазонов значений переменных или массивов?

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

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

Если требуется объявить идентификатор, совпадающий с ключевым словом

Перегрузка переменных

кортежи могут неявно раскрываться и наоборот, неявно формироваться

заявлена поддержка перегрузки по возвращаемому значению

Больше. Неявных. Способов. Выстрелить. В. Ногу.

Эта лошадь давно сдохла, слезайте.

Оператор возведения в степень

Во многих языках пытаются его ввести, и везде придумывают разные операторые символы (^, ** и т.п.). На этот раз обратный слэш.

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

3.141_592 654; // floating constant

Как вот это понять? Что здесь есть "654"?

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

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

PI = 3.141597

У вас ус отклеился.


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

with взят скорее из ДжаваСкрипта, а не из Пайтона.

Да нет же, - классический Pascal. Кстати, возможно это действительно было бы удобно в случае ручного заполнения всяких C-подобных WinAPI-шных зубодробительных структур типа PIXELFORMATDESCRIPTOR.

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

With MyLabel 
 .Height = 2000 
 .Width = 2000 
 .Caption = "This is MyLabel" 
End With 

А как там с type aliasing? видимо, как в Си. Нельзя, но если использовал, то у тебя больше не программа на Си.

>Одну структуру можно встроить в другую так, как это реализовано в Go (и даже лучше - используется ключевое слово inline). Это очень простая и в то же время мощная концепция, прямо готовая для proposal'а в очередной стандарт С и/или С++... Удивительно - почему ее сразу не сделали в Си?

В С++ это уже давно есть, только называется немножко иначе - наследование :)

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

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

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

Базовых классов может быть несколько.


struct PERSON_DATA
{
    int Age;
    int Weight;
};

struct LIST_ELM
{
     void* Prev;
     void* Next;
};

struct PERSON_ELEMENT : LIST_ELM, PERSON_DATA {};

PERSON_ELEMENT унаследован от PERSON_DATA, при этом PERSON_DATA не находится в самом начале PERSON_ELEMENT.

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

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

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

Эх, молодежь!...

Почитайте описание конструкции "like" в структурах PL/1. Не прошло и 55 лет, как опять придумали "удобную фичу".

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

UFO just landed and posted this here

В своё время удивило, что внутри switch нельзя goto к case из кода другой case в этом-же switch, хотя по сути это метки для goto-прыжка. Пришлось писать явно.
Здесь этого тоже нет?

В C# есть goto case, в других языках я такого не встречал.
На обычно C/C++ без проблем можно
switch(arg) {
	case 0: case_0: { 
	} break;
	case 1: {
		goto case_0;
	} break;
}

Лучше бы очередной фреймворк для JS сделали. И то больше бы пользы было для пацанов.

Sign up to leave a comment.

Articles