Pull to refresh

Comments 29

В последнем примере звёздочки не потерялись? (фраза "что & и взаимоуничтожаются" намекает)

UFO just landed and posted this here

Я вам страшный секрет открою: единственное что здесь можно "притянуть" к C++ это использование ссылки в последнем примере. Остальное - чистейший C.

Вы кода на C++ никогда не видели?

UFO just landed and posted this here

#define nullptr ((void*)0)

Такое часто можно встретить в новом C коде. Более того в C23 введут nullptr, так как обычный NULL по стандарту может иметь тип int.

UFO just landed and posted this here

А с чего это C++? Кроме последнего примера с ссылкой ничего плюсового тут нет. Код стайл абсолютно сишный.

"надефайнить" что угодно не получится, попробуйте "надефайнить" шаблоны или лямбды. А вот nullptr в C как раз хорошо дефайнится, так как от void * неявный каст всегда. В C23 nullptr тоже предложен через define.

Так что ваше первоначальное "код везде на C++" - не верно. Код на C++ явно только в последнем примере. Всё остальное - вполне себе сишный код в типично сишном стиле.

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

Хотелось бы посмотреть на код, где-нибудь в godbolt, где компилятор подобным образом оптимизирует выход за границу массива

Ключевое слово «может». А пока, насколько я понимаю, промышленные компиляторы оптимизируют не так прямолинейно.

Ну и код плюсовый, но это мелочи.

Плюсовый он только в последнем примере, где есть ссылки. Остальное выглядит как современный C код. Bool ещё в далёком C99 ввели, nullptr часто сами программисты через define вводят, так как NULL может иметь тип int.В C23 nullptr частью стандарта станет. А какие плюсовые моменты кроме ссылки в последнем примере есть?!

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

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

UFO just landed and posted this here

Из примера непонятно, что мешает компилятору проверить условие выхода за границы статического массива? Как по мне в таких случаях должны использоваться как минимум варнинги вместо непонятных оптимизаций. И вот если программист укажет явно, что хочет именно этого, тогда уже оптимизация. Но в этом случае опять же она будет заведомо не верной. Раз программист считает, что в пятом элементе массива что то есть и код без ошибок, значит компилятор должен это учесть. Он не должен считать себя умнее программиста (пока... Вот подрастëт ещё, тогда посмотрим))

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

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

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

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

Конечно, легко ссылаться на несколько сотен страниц стандарта, требовать изучить его и знать наизусть, говорить, что UB=ССЗБ. Но язык стал бы намного дружелюбнее, если компилятор не превращал UB-код в машину времени и не форматировал диск втихомолку, а отмечал бы странные места для более тщательного исследования.

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

Мне трудно представить штатную ситуацию, когда у меня в коде есть цикл с условием внутри и компилятор это может полностью выкинуть. Такой код определённо нуждается в исправлении.

Ждал, каким будет перевод «unrung the bell» на русский. «Отменил прозвеневший звонок» — ну тоже ничего.

Имхо, объяснение неверное, или я его не так понял. Компилятор всегда предполагает, что UB в коде нет. Он может так считать, потому что, если оно есть, то любое поведение корректно. Так как UB нет, то цикл не может дойти до индекса i == 4. Единственный вариант не дойти до i == 4 - выйти с true раньше. А дальше уже удаляется ненужный код, так как он не имеет побочных эффектов.

Спасибо за объяснение!

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

Автору статьи, кмк, стоит исправить свое объяснение на Ваше.

Я бы объяснил так, при индексе >= 4 - уб. Тогда i всегда < 4, тогда цикл бесконечный. В этом бесконечном цикле только один выход - return true, значит он когда то и случиться, можно оптимизировать до return true

Тут еще более тонко! Бесконечный цикл без побочных эффектов в C99 тоже UB! А поскольку компилятор знает что программист не допускает UB — то цикл является конечным, и так как все выходы из него возвращают true, то… — и далее по тексту. В C11, кстати, не являются UB бесконечные циклы с константным условным выражением. То есть while(1) — как бы снова запретили вырезать из кода. Но на приведенный пример это, увы, не распространяется. :-(

Мой личный топ это https://habr.com/ru/company/infopulse/blog/338812/

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

Я могу только в очередной раз сказать, что текущее понимание «UB» производителями компиляторов — суть злоупотребление правом. Дух (но не буква) стандарта языка «C» в отношении «UB» заключался в том, что разработчики стандарта не видели (!) возможности навязать определенное универсальное поведение в таких случаях (например, доступ за границей массива) без неприемлимых потерь производительности. Поэтому в таких случаях они оставили поведение неопределенным. Мне кажется, что в то время имелось в виду — что создатели стандарта языка не дают никаких гарантий поведения в такой ситуации, и вы должны обратиться к руководству по своему компилятору и аппаратной платформе, чтобы понять — как будет вести себя скомпилированная данным компилятором на данной платформе программа. И да, если вы закладывались на определенное поведение компилятора/платформы в случае UB — это делало вашу программу не переносимой (впрочем, обойти это условной компиляцией это тоже никто не мешал...).

С тех пор прошло много времени. Некоторые считают, что разработчики компиляторов оказались рабами синтетических тестов. Я считаю, что виноват C++ и мета-программирование на шаблонах приведшее к чрезвычайной популярности техник dead-code elimination. Отягчающим обстоятельством является также то, что почти все существующие компиляторы кросс-платформенные, а иметь разные стратегии кодогенерации и оптимизации под каждую платформу — довольно дорого…

В общем, в какой-то момент разработчики компиляторов злоупотребили предоставленным им в рамках стандарта правом генерировать любой эффективный на данной платформе код в случае UB — и объявили что в случае UB они считают эффективным вообще ничего не генерировать. Нельзя сказать, что такое случается только в мире «C/C++». Так, например, в конституции одной знакомой нам страны был заложен запрет президенту занимать свой пост более двух сроков подряд. Однако, сначала один знакомый нам конституционный суд ослабил запрет до «двух сроков подряд, а если не подряд — то уже можно», а потом в результате неких юридических манипуляций запрет был признан как бы вообще не существующим… Так что у разработчиков компиляторов есть на кого равняться… Люди — они такие люди…

Что с этим делать? Есть несколько способов. Можно собраться и определить в новой версии стандарта правильное поведение во всех упомянутых случаях — так поступает, например, Паскаль. Ценой будет падение производительности на многих (или всех) платформах. Непонятно, кому такой хоккей будет нужен?..

Чтобы избежать этого — можно определить абстрактную среду исполнения, и раскрыть UB в терминах этой среды (оставив компилятору и виртуальной машине свободу в генерации кода для конкретной платформы). Это будет примерно Java. Со всеми ее достоинствами и недостатками.

Можно не делать ничего, и стараться писать код без UB. Это будет примерно C++, потому что без dead code elimination — он не жилец. А специфичные для платформ оптимизации придется прятать в библиотеки или интринсики.

Для «C», с моей точки зрения, следовало бы принять поправку к стандарту, обязывающую создателей компилятора специфицировать поведение в каждом случае UB на конкретной платформе, или (если они не могут этого сделать по объективным причинам — например, таковое определяется конфигурацией аппаратуры или операционной системой) — описать код, который они обязуются генерировать в таком случае. Мне довольно трудно представить себе вменяемого разработчика компилятора, который во всех случаях UB в «C» сможет написать «стереть файлы с диска» или «удалить всю функцию из кода». В любом случае, в интернете будет кому объяснить такому публично (и, возможно, нелицеприятно) всю неправильность его позиции… А один раз описав (и обосновав!) контракт компилятора по генерации кода в случае UB на конкретной платформе — можно ожидать что разработчики будут связаны этим контрактом (и обоснованиями) на достаточно долгий срок — что обеспечит стабильность (но не переносимость!!!) поведения кода на языке «C» и сохранит его как высокоуровневый, эффективный, и почти переносимый ассемблер. Что и отличает его до сих пор от Паскаля, Джавы и прочих конкурирующих языков программирования…

Зачем гадать, что имели в виду создатели стандарта, если они сами разъяснили, что каждый термин означает в C Rationale Document.

Unspecified behavior gives the implementor some latitude in translating programs. This latitude does not extend as far as failing to translate the program.

Undefined behavior gives the implementor license not to catch certain program errors that are difficult to diagnose.

Implementation-defined behavior gives an implementor the freedom to choose the appropriate approach, but requires that this choice be explained to the user. 

Был случай, генерировал на си ланге вчар стрингу со всеми юникод(почти всеми возможными) проще говоря, были все числа от 0 до максимального для вчар символами компилятор в релизе, просто игнорировал и возвращал строку на 5 символов, при повторном вызове на ~53 000, хотя по факту должно было быть больше

Sign up to leave a comment.