Комментарии 96
Мне кажется, что из-за несовершенства человеческого языка, которым пишутся стандарты — у нас произошло огромное недопонимание между авторами компиляторов и всеми остальными пользователями.
Когда умные люди делали языки (Керниган, Ричи, Страуструп) — они понимали, что не могут предусмотреть заранее все возможные архитектуры на которых будут реализованы компиляторы и рантайм языка. Чтобы не связывать по рукам и ногам будущих разработчиков ЭВМ, компилятора и рантайма — они сознательно отказались покрывать спецификацией "странные пограничные случаи". Так родилось понятие "UB" — которое означало в те времена, что результат определяется архитектурой ЭВМ и особенностями компилятора. То есть язык вам не гарантирует что инструкция закрытая под if (x+1>x) выполнится при определенном значении X. Но в те времена — это не означало что соответствующее условие вместе с веткой кода можно просто выкинуть. Соответствующий код генерировался — но узнать, как он себя поведет — можно было только запустив исполняемый файл на вашей конкретной ЭВМ. Например, если аппаратура ЭВМ имела аппаратное детектирование переполнений, то код под веткой if мог действительно не выполнится — но не потому что компилятор его обнулил, а потому что с точки зрения языка — такая ситуация не определена. Однако, на конкретной платформе — поведение программы с UB было вполне определенным: она либо выполняла соответствующий кусок кода если происходило переполнение регистра, либо падала в ОС с диагностикой.
Потом авторы компиляторов решили толковать "что угодно" расширительно. Это позволяло конкурировать в синтетических тестах с другими авторами компиляторов и применять оптимизации все ближе и ближе к грани фола. Апофеозом стало темплейтное метапрограммирование на C++, имеющее тенденцию порождать кучу специализаций шаблона (SFINAE) которые никогда не будут использованы, и как следствие — резкий уклон компиляторов C++ (а следом и C — потому что обычно это один и тот же компилятор под капотом c разными настройками) в попытки доказать что некий код — unreachable и его можно вырезать.
Грубо говоря — если раньше компилятор воздерживался от оптимизации в случаях когда не мог с уверенностью доказать что программа будет эквивалентна тому что написал программист — то теперь ситуация обратная: это вы должны доказать компилятору что он не может из вашей программы сделать нечто иное — чего вы не писали.
Да, понятно что с другой моделью оптимизации C++ — не жилец. Да, понятно, что мы сейчас начнем ныть что разработчики приходят с низкой квалификацией и нельзя от них ожидать чтобы они думали о ручной оптимизации (выносы инвариантов и проч). Но когда по статистике больше 70% проектов на C++ содержат хотя бы одно UB (и только вопрос времени, после какой очередной версии компилятора оно стрельнет!) — и если программа на C++ ведет себя не так — мы вынуждены лезть в ассемблер чтобы понять, в какую сторону ее вывернул компилятор, и обратным ходом искать UB которое ему удалось использовать… Я не знаю, считать ли это прогрессом, или наоборот...
P.S. В результате, я лично перешел для большинства задач на Java (где UB гораздо меньше), а C++ волевым решением ограничил до относительно безопасного подмножества включающего "С с классами" и шаблоны для быстрой генерации специализаций классов (и предотвращения копипасты), но исключая темплейтное мета-программирование.
результат определяется архитектурой ЭВМ и особенностями компилятора
Вы путаете undefined behavior и implementation-defined behavior.
С остальным комментарием в целом согласен, программирование на C++ - это хождение по минному полю.
Я все-таки буду настаивать на своем. implementation-defined behavior — это когда авторы компилятора и рантайма обязаны выбрать определенное поведение для целевой платформы и гарантировать его. UB — это когда компилятор генерирует код, но виртуально пожимает плечами: "хрен знает, зачем вы это делаете — смотрите сами что получится когда вы это запустите". Технически, результат может быть даже разный при разных опциях оптимизации (а в случае implementation-defined behavior — вообще говоря должен быть какой-то один). Однако, изначальное понимание UB было: "мы вам генерируем код как вы сказали, но стандарт языка это не покрывает, так что рассчитывайте только на то что вы лучше нас знаете свое обрудование и операционную систему". Теперь UB понимается в лучших традициях школоты: "… училка заболела, айда гулять с уроков!".
Есть два UB - undefined behavior и unspecified behaviour. Ваш пример с умными указателями - это как раз unspecified behavior, то есть имеется высокоуровневое описание поведения, но нет формального описания внутрненнего поведения, которое бы разрешило спорные моменты конкретных реализаций. Undefined это когда конструкция вполне легальна, но поведение в особых случаях никак не определяется вовсе и зависит от конкретной реализации.
Конструкция легальна, но при этом компилятор будет предполагать, что программист абсолютно точно всё проверил, и Undefined Behaviour в программе отсутствует. А если нет проверок в коде, значит гарантирует, что входные данные проверены. То, что программист мог просто ошибиться, компилятор не предполагает :D
И как раз эти предположения приводят к очень...интересным изворотам программы
P.S. В результате, я лично перешел для большинства задач на Java (где UB гораздо меньше)
Строго говоря, в джаве UB нет вообще, и даже в JLS такое слово не встречается. :) Дело в том, что "потенциально опасные" инструкции в джаве всё равно могут быть. Например, если вы компилируете джаву в LLVM (как делает компилятор Falcon), у вас всё равно в какой-то момент могут появиться всякие nsw флаги. Другое дело, что (если только в компиляторе нет багов) в этом случае реальное, непосредственное UB никогда не должно выполниться. Однако poison вполне себе генерится и протекает.
Цикл while (true) {} — это бесконечный цикл без сайд-эффектов, то есть UB.… Можно просто удалить весь последующий код вместе с return'ом.
Вообще когда смотрю на это то кажется что стандарт пишут наркоманы эффективные менеджеры, а не инженеры.
Переполнения знаковой арифметики
Какие проблемы сделать модификаторы типов для определения поведения? И типы длинных чисел.
Деление (или взятие остатка) на 0
То есть явно указать поведения тоже невозможно средствами языка. Опять что-то мешает?
..(например, за границы массива)
Что мешает сделать проверки в отладочных билдах и а релизе предполагать что они выполняются. Более того введение инвариантов, которые сохраняются до и после операций могло бы решить часть этих граблей. И явно инструкциями в коде писать что такие значения не возникнут, а не выводить подобное косвенными путями с нарушением математической логики.
Использование не инициализированных переменных
Что мешает инициализировать переменные нулями. И если кому явно надо не инициализированные данные то это указывать явно как в zig
Бесконечный цикл без сайд-эффектов
Бесконечный цикл не надо выкидывать, его надо преобразовывать в вызов команды ожидание завершения потока.
Вообще там огромное количество преобразований которые не основываются на логике и здравом смысле. Более того для указателей вводится происхождение, которое не имеет представления в коде программы. Вот кроме как вредительством это назвать нельзя.
Более того если затронут подобную тему, тут же начинают говорить что вы ничего не понимаете в не бинарных гендорах в компиляторах. Компиляторы которые вредят опасны. Стандарт C++ поощряет именно такое поведение.
Вообще когда смотрю на это то кажется что стандарт пишут
наркоманыэффективные менеджеры, а не инженеры.
Вы не поверите, но стандарт как-то так и производился - крупные и не очень компании собирали консорциумы и пытались прийти к единому мнению как что-то должно быть сделано. И при этом ещё и обратная совместимость добавлялась с более ранними версиями и Си. И вот совместимость портит всю малину по сию пору.
Какие проблемы сделать модификаторы типов для определения поведения?
Первый кандидат на проблемы с совместимостью с Си.
Что мешает инициализировать переменные нулями.
Второй кандидат. Во-первых - это медленно. Во-второых именно для этого в C++11 как раз завезли агрегатную инциализацию, но писать две лишних скобки очень сложно приучить, опять же учитывая совместимость. Проверки на unititialized вроде в ворнинги не так давно только добавили, а то и вовсе только во внешних анализаторах кода живут.
Что мешает сделать проверки в отладочных билдах
В стандартных контейнерах так всё и сделано. Но многие все ещё тяготеют в пользу простых сишных массивов. Отсутвие span
в стандартной библиотеке также играет свою роль. Всякие view-like механизмы только с C++17 стали появляться.
Вот кроме как вредительством это назвать нельзя.
А вы видимо умеете заглядывать на пол века вперёд, чтобы предсказать как ваш код будут использовать? Раньше плюсы были вообще препроцессором над Си, который превращал плюсовый код в Си, а потом уже начинал его компилировать (гуглить cppfront). И только спустя 15 лет появился некоторый стандарт. Хорошо говорить с колокольни, когда потрогал уже несколько десятков языков. А так да, спонсор этого вашего вредительства последние сорок лет.
Отсутвие
span
в стандартной библиотеке также играет свою роль.
У span как раз-таки, в отличие от контейнеров, проверок выхода за границы нет - функция at() отсутствует.
Первый кандидат на проблемы с совместимостью с Си.
C++ не совместимо с C. И потом что модификаторы могут испортить?
typedef int __module_arithmetic__ int_m;
typedef int __overflow_checking__ int_o;
typedef int __modification_callback__( invariant1_validation_fn ) int_inv1;
инициализировать переменные нулями… Во-первых — это медленно.
Так если требуется явно указывать что не инициализировать. А не делать чудные оптимизации, о которых договорились крупные и не очень компании.
bool question() { bool tobe; return tobe || !tobe; }
В c++ bool имеет минимум 3 значения true=1,false=0 и UB=не представима в коде целевой программы. То есть за значением переменных таскаются пачки метаданных которые активно используются оптимизатором, но при некоторых преобразования могут отваливаться. Что вызывает новые UB.
В стандартных контейнерах так всё и сделано
Стандартные контейнеры просто частный случай. Для отладки вообще можно специальную виртуальную машину использовать, что бы все выходы за границы контролировать.
А вы видимо умеете заглядывать на пол века вперёд...
Тут не надо быть ясновидцем. Даже школьнику видны ляпы в заложенной логике. Если код раньше использовал модульную арифметику на int. То при включении оптимизации он превратится в тыкву, хотя пол века назад всё прекрасно работало.
c++ bool имеет минимум 3
Имеет ровно два значения. А если вы трогаете неинициализированные значения, то самостоятельно стреляете себе в ногу. Оно потому и UB, что гадать и предсказывать поведение сложно и ненужно, а уж тем более закладываться на это поведение.
А не делать чудные оптимизации
вы просили быстро - вам сделали быстро. Это значит всё лишнее и ненужное выкинуто. Даже пруф Великой Теоремы Ферма. Какие вопросы?
Если код раньше использовал модульную арифметику на int.
В том-то и суть, что зоопарк архитектур примерно никак не гарантировал поведение при переполнении. А т.к. изначально оно генерировалось в Си и под каждую архитектуру люди изобретали свои отдельные компиляторы под каждую же архитектуру имелось определенное количество багов тех компиляторов. И вот это ваше "раньше использовал" работало не всегда и не так прекрасно как вам хотелось бы верить. Это сейчас всё относительно изоморфно c х86_х64 en masse. Это уже не говоря про то что современный школьник сейчас получает более полные и обобщенные знания информатики, чем многие программисты того времени могли представить, раз они нынче такие умные, что видят ляпы в языках программирования.
Цикл while (true) {} — это бесконечный цикл без сайд-эффектов, то есть
UB.… Можно просто удалить весь последующий код вместе с return'ом.
Вообще конкретно это UB довольно логично и служит для помощи программисту.
А вот компилятор, так зловредно его эксплуатирующий... ну выпускнику MIT надо в резюме воткнуть строчку "мой коммит в LLVM ускорил specperf_*** на 0.01%.
Логика тут примерно такая:
1. Любой код без side-effects можно (и нужно в целях производительности) удалять.
2. Кроме бесконечного цикла - который сам по себе side effect.
3. Но если мы не можем доказать про цикл, что он конечный/бесконечный (и цикл без side-effects) - давайте его удалим. Пользы намного больше чем вреда.
3.1 при этом явно скажем программисту, что бесконечные циклы без других side-effects запрещены
============ а вот дальше идёт довольно-таки плохая логика
4. а давайте удалять те циклы, которые без side-effects даже явно бесконечные, формально ссылаясь на то, что это UB и его быть в коде не должно.
4.1 В оправдание п.4 - c развинием компиляторов многие циклы про которые раньше было непонятно finite\infinite стало можно сделать выводы. Поэтому если не принимать "4" - то для некоторых других существующих программ с обновлнием компилятора получим изменение поведения. В общем как не крути всё плохо.
ПС
> для бесконечного цикла надо давать команду ожидания окончания потока.
В Rust (примерно) так и сделано.
Цикл loop {} именно бесконечный с гарантией что не удалится бэкэндом.
В С \ С++ часто сложно сделать что-то разумное не поламав обратную совместимость.
Вообще когда смотрю на это то кажется что стандарт пишут
наркоманыэффективные менеджеры, а не инженеры.
Не сомневаюсь, вы дизайните языки и пишете стандарты лучше, чем спек-комитет. Но данная оптимизация очень даже не бессмысленная. Представьте такой код:
while (true) {
<килотонна кода>
cond = ...
if (cond) break;
<килотонна кода>
}
Представьте, что компилятор доказал, что cond начиная с итерации 10 -- инвариант. И переписал цикл в духе
int fake_cnt = 0;
while (true) {
<килотонна кода>
cond = ...
if (cond && cnt++ < 10) break;
<килотонна кода>
}
cond_10 = ...
while (true) {
<килотонна кода>
if (cond_10) break;
<килотонна кода>
}
Теперь у вас во втором цикле инвариантное условие, и его можно переписать как
cond_10 = ...
if (cond_10) {
while (true) {
<килотонна кода>
if (true) break;
<килотонна кода>
}
} else {
while (true) {
<килотонна кода>
if (false) break;
<килотонна кода>
}
}
Допустим, cond_10 в реальности всегда true, но доказать это невозможно. Так бывает. Теперь у вас второй while -- бесконечный цикл с горой кода, который ничего не делает. Если вы туда зайдёте, ваша программа зависнет и не сделает ничего и никогда. Вычищение такого кода -- экономия компайл тайма.
Какие проблемы сделать модификаторы типов для определения поведения? И типы длинных чисел.
Вы, надеюсь, понимаете, чем сложение в длинных числах отличается от сложения в типах, поддерживаемых аппаратно, и во столько раз это медленнее? Если нет, то марш читать про то, что такое регистры.
То есть явно указать поведения тоже невозможно средствами языка. Опять что-то мешает?
То есть, вы предлагаете перед каждым делением вставлять (на уровне компиляторного фронта) чек типа
if (denom == 0) {
// делай то, чего вы там хотите
}
? Ну тогда будет Java, пишите на ней. В С++ это убьёт перфоманс.
Что мешает сделать проверки в отладочных билдах и а релизе предполагать что они выполняются.
А что будет, если вы предположили, а они не выполняются? :)
Что мешает инициализировать переменные нулями.
Помимо того, что дорого, так ещё и убьёт кучу оптимизаций. Ну например:
int x;
if (smth) {
x = 10;
}
print(x);
Вы тут не сможете заменить на print(10), придётся честно проверять.
Бесконечный цикл не надо выкидывать, его надо преобразовывать в вызов команды ожидание завершения потока.
Уточните -- кому надо? :)
Вообще там огромное количество преобразований которые не основываются на логике и здравом смысле.
Если вы не понимаете, зачем это делается, это не значит, что в этом нет логики и здравого смысла.
Более того если затронут подобную тему, тут же начинают говорить что вы ничего не понимаете в
не бинарных гендорахв компиляторах.
Верно, вы ничего не понимаете в компиляторах. Вас это удивляет? :)
Представьте такой код
Вы вообще не в ту сторону смотрите. Я не хочу представлять код. Я хочу иметь возможность работы с кодом как и с другими типами данных. Что бы были селекторы, фильтры и возможность выполнять явные преобразования, что бы его можно не только генерировать но и анализировать и итерационно модифицировать. А не сваливать это невменяемый компилятор которые выполняет преобразования на основе стандарта, который ложит кладёт болт на формальную логику, там где это ему хочется.
Вы, надеюсь, понимаете, чем сложение в длинных числах отличается от сложения в типах
Вы надеюсь издеваетесь. Вы когда со строками работаете вас это не останавливает.
Тут тоже самое надо быстро int надо не без заморочек number.
Если вы не понимаете, зачем это делается, это не значит, что в этом нет логики и здравого смысла.
Если вы не понимаете что если у вас дырявая аксиоматика, то и теория будет полной лажей, то что тут поделать. На всякий случай напомню что компилятор это инструмент для инженера что бы упростить создание бинарных программ в условиях имеющихся ограничений. А когда инструмент не консистентен, то он обрастает кучей костылей work around-ов и ub.
Вы вообще не в ту сторону смотрите. Я не хочу представлять код. Я хочу иметь возможность работы с кодом как и с другими типами данных. Что бы были селекторы, фильтры и возможность выполнять явные преобразования, что бы его можно не только генерировать но и анализировать и итерационно модифицировать.
Понял. Пишите на лиспе.
После таких статей начинает казаться, что программы на C++ работают только чудом, если мысли разработчика и компилятора совпали.
А как писать на C++ без UB?
А как писать на C++ без UB?
Просто смириться, что ub - таже ошибка, что и не ub. На какой-нибудь яве можно случайно базу стереть неправильным sql запросом. Врядли ub прям сильно страшнее
Знать что они существуют, следовать гайдлайнам и делать хорошо и не делать плохо.
Собственно, писать без UB :-)
Обратите внимание – примерно во всех примерах из статьи (кроме разве что бесконечного цикла) программист знает про UB, но делает какие-то предположения о том, во что их превратит компилятор. Надо от этого отучаться.
ЗЫ: а авторам компиляторов и статических анализаторов – развивать диагностику. Довольно часто можно определить, что программист не учёл или неправильно обработал возможность UB. Классический пример – проверка if (this), встречавшаяся раньше в коде от M$ (и которую clang просто выкидывает, выдавая warning).
То есть для того, кто работает с плюсами только время от времени - обложиться анализаторами по полной?
Ну, это полезно, но я не об этом. "Не допускай переполнения", "не выходи за границы массива", "не вызывай функции объекта, если он может быть nullptr" и всё такое. Если по простому – ставьте всюду явные проверки, и убирайте их, только когда сами точно знаете, что всё Ok (кстати, есть шанс, что их за вас уберёт компилятор).
По мне – можно даже не проверять по стандарту, нарушается правило "пиши код так, будто поддерживать его будет склонный к насилию психопат, знающий, где ты живёшь". Увидев код – дал бы автору по рукам.
А просмотрев – думаю так же, как вы: базово – нельзя, но можно поискать явные разрешения (если найдутся – обязательно вынести в комментарий, ибо см. правило из первого абзаца).
Собственно, писать без UB :-)
То есть, чтобы писать без UB, надо писать без UB? Получается какая-то бесконечная рекурсия...
...поведение которой тоже неопределено :DDDD
Собственно, писать без UB :-)
Вы давно по минному полю ходили. Надо просто не наступать на мины. А если вы не один, а целая стая?
Дык возникающее UB – это, как правило, ошибка в программе (переполнение, разыменование нулевого указателя и т.п.). Просто мы привыкли, что компилятор нас "прощает" и закладываемся на наши представления о поведении компилятора в этих случаях. Надо отвыкать.
Причём, как правило, мы эти UB видим. Просто действуем по принципу "и так сойдёт". Ну типа переполнение – ничего страшного, просто получим неверный результат, потом обработаем. Вот и в статье – все UB видны сразу (кроме разве что бесконечного цикла).
А для "стаи" – писать guidelines и бить палкой за их нарушение. К примеру, требовать писать контракты функций и соблюдать их "внутри и снаружи" (функция на допустимых данных должна корректно отработать [exception, описанный в доке на функцию – тоже корректно], а вызывающий код должен предоставить корректные данные [если пришли снаружи – проверить и обработать ситуацию некорректных]).
Можете почитать статью Джона Рейгра Undefined Behavior != Unsafe Programming. Это не поможет писать без UB (это невозможно), но может, вы станете к этому относиться проще. :)
После таких статей начинает казаться, что программы на C++ работают только чудом, если мысли разработчика и компилятора совпали.
Если ваша программа на C++ работает без ошибок. Обратитесь к разработчику компилятора, он исправит ошибки в компиляторе.
Пишите как обычно, без оптимизации, но с пониманием системной архитектуры. В этом случае поведение будет чётко определено. Вообще, вся концепция UB нужна ради оптимизации. Без оптимизации, к примеру, результат целочисленного переполнения совершенно предсказуем и легко детектируем с помощью флагов процессора (OV).
Спасибо.
Один интересный вопрос остался не покрытым.
В С и С++ разные наборы UB - как бэкэнд LLVM это разруливает?
Скорее всего это разруливает фронтенд компилятора, генерируя для LLVM подсказки: вот тут можно соптимизировать, а вот тут нельзя
Это разруливает фронтэнд. LLVM IR имеет чётко прописанное поведение каждой своей инструкции (когда там poison и когда там UB), а задача компилятора С или С++ -- перевести конструкции этих языков в LLVM IR таким образом, чтобы они работали так, как требуют стандарты этих языков.
Именно каждой? Т.е., грубо говоря, чтобы избежать UB в конкретной инструкции – достаточно прямо перед ней вставить (автоматически) проверку аргументов и бросать exception, если они некорректны? (Причём изрядную долю этих проверок оптимизатор выпилит).
Т.е. можно задёшево и независимо от языка сделать код для тестов на UB?
Вы можете генерить такой LLVM IR, в котором вообще не будет UB. Например, так, как вы описываете (плюс не навешивать никуда флагов типа nsw). Даже там, где язык это позволяет, он не обязывает делать именно UB. Но оптимизатору с этим будет жить тяжелее, поэтому на практике всегда, когда язык позволяет UB, стараются сгенерить такой IR, который ведёт себя так же.
Т.е. можно задёшево и независимо от языка сделать код для тестов на UB?
За очень-очень-очень дорого, но можно. Можете померить скорость работы джавы с отключенным Tier 2 компилятором и сравнить с аналогичными программами на С++. Будет примерно то же самое (ожидаю в среднем разницу раз в 5-10, в терминальных случаях в сотни и тысячи).
"Задёшево" – в смысле усилий программиста. Прогнать тестирование на 10% производительности (особенно если есть возможность для тестов взять железо попроизводительнее) – приемлемо.
Ну грубо говоря как когда-то в Turbo Pascal можно было собрать прогу с проверкой выхода за границы массива и т.п. – для отладки собирали так, а деплой уже без этого.
В смысле усилий программиста дешевле UB sanitizer. Он эти чеки сам вставляет. Другое дело, что так можно отловить сильно не всё. Вручную, впрочем, тоже. Банальный пример: если у вас есть только int*, вы никак не можете проверить, вылезаете вы за границы выделенной памяти или нет. Для этого где-то дополнительно придётся хранить длину, и это будет работать только если указатель всегда только на начало массива, а не куда-то в середину.
Можно просто удалить цикл и весь последующий код вместе с return'ом.
Почему тогда компилятор этого не сделал а оставил код от never_called()? Разве неиспользуемый код не удаляется компилятором при оптимизациях?
never_called не была помечена как static.
А значит обязана остаться в единице компиляции.
ПС
Но вообще требовать от компилятора удалить неиспользуемый код довольно странно.
Но вообще требовать от компилятора удалить неиспользуемый код довольно странно.
Я на С++ не писал, поэтому возможно мои вопросы выглядят странно или глупо.
Но для меня "удивительно" что компилятор выкинув код перешел по сути к пустому main() без кода, ведь по утверждению автора компилятор удалил оттуда код. Но изначально пустой main компилируется в нечто совершенно другое(с вызовом ret).
Какое именно поведение тут пытался сохранить компилятор?
С точки зрения компилятора такого main
а "не может быть", потому что бесконечный цикл не "можно удалить", а "приводит к неизвестным последствиям". То есть, компилятор считает себя уверенным в том, что main()
никогда не вызовется. Поэтому весь его код можно удалить.
Если бы main
была while(false)
, компилятор бы (скорее всего) соптимизировал main()
в return;
При этом и main
и never_called
- экспортируемые метки объектного файла, поэтому должны существовать. И так случилось, что main
== never_called
. При этом последняя - нормальная функция, и её компилятор компилирует как надо.
Какое именно поведение тут пытался сохранить компилятор?
Спасибо за вопрос. Он отличный.
Всё, что сделал компилятор - он сделал чисто технически боле-менее корректно:
1. Подкорректировал main (удаление return - несомненно malevolent действие) из-за UB, право имел.
2. Сохранил never_called (она не статик => её чисто теоретически мог вызывать кто-то другой извне).
3. Сложил 2 функции в машинных кодах в исполняемый файл, легли подряд.
Вы спросите: так бю... а виноват кто.
Виновата концепция UB, которая делает FAIL-SLOW (т.е. она явно говорит - что-то может пойти не так, как угодно плохо, но до конца пытается сделать вид, что UB это не ошибка, и может ещё удастся вырулить и всё наладится) - и в итоге в плохом случае портит вообще всё.
Сейчас эту проблему осознали. В новых языках, даже низкоуровневых UB нет. Например Rust - даже по производительности он не медленнии С\С++.
Проблема со старыми языками. Там от UB избавитсья крайне сложно.
Малой кровью - надо было бы запретить return в этм месте. Но тут надо копать кишки LLVM.
Спасибо за объяснение. В общем то пункт 2 понятен, пункт 3 скорее следствие. Но пункт 1 по прежнему загадка.
Можно конечно просто принять ответ потому что UB, но это не объясняет почему именно так произошло.
Если считать что компилятор пытается в случае UB выжать максимальную производительность то я бы меньше удивился если бы он выкинул все метки и вообще весь код. По крайней мере это было бы адекватнее и объяснимее. Даже если это нарушает ожидания "её чисто теоретически мог вызывать кто-то другой извне", у нас же тут UB а значит полная индульгенция.
Выкидывание ret из main точно так же нарушает стандарт c++, где в случае отсутствия return statement стандарт требует implicitly return the value 0
.
Чисто технически могло быть так (это пишут выше):
1. В компиляторе есть микро-фаза DCE (dead code elimination) - она удаляет весь код, до которого доказанно не дойдёт управление (и чисто транзитивно - весь доминитуемый "мёртвым кодом" код - тоже мёртвый).
2. Доказанно встретили UB - пометили цикл как DeadCode.
3. После какой-то фазы вызвали DCE - удалили сам цикл и всё что им доминировалось.
Честно говоря, это выглядит как будто компилятор решил исполнить "итальянскую забастовку".
Было бы гораздо лучше, если бы DCE
- либо никогда не удалял единственную ветвь исполнения кода;
- либо заменял такую ветвь на безусловный бесконечный цикл;
- либо удалял бы эту ветвь вместе со всеми ссылками на неё.
А так получается сидение сразу на двух стульях: с одной стороны, код в main никогда не вызывается а потому не нужен — с другой стороны, символ main остаётся на всякий случай, вдруг он кому-то всё же нужен.
Вроде и по стандарту всё, а всё равно хренью ощущается.
Как в анекдоте:
Ни рубля не топора, но вроде всё правильно.
Самое интересное что оно не только ощущается так, оно этим и является. Но почему-то огромное количество мух человеков считают что так и должно быть.
Каждый раз будем спрашивать "if is_only_main_path() {...}" - попытка заткнуть решето.
Проблема именно в концепции UB и довольно злокачественном её использовании в погоне за +0.(0)х% производительности. Собственно от неё и надо бы избавляться.
DCE - важная и нужная фаза и для скорости компиляции и для качества результирующего кода.
Говоря про раст, надо уточнять, про какой именно. UB отсутствует в safe-подмножестве, во всяком случае это цель, и обратное считается багом
Однако при использовании unsafe-подмножества UB все ещё присутствует, и если нарушить инварианты (пишутся в документации отдельным заголовком Safety), то ноге будет больна
Посмотрите на ассемблер внимательно. Там две разные функции, просто записанные подряд. Тело одной начинается с лейбла main:
, а тело другой -- с never_called():
. Просто так получилось, что весь код после лейбла main:
выкосился, и поэтому при передаче туда управления немедленно начинает исполняться код другой функции, который лежит дальше.
Прочитав объяснения выше, примерно понятно, как "рассуждал" компилятор, и как он до этого докатился. Но, давайте отбросим все промежуточные выкладки, и просто посмотрим на конечный результат:
- мы имеем исполняемый файл
- в исполняемом файле есть функция
main()
, она никуда не выкинута - функция — это метка, на которую можно передать управление инструкцией
call
, и потом получить управление назад по инструкцииret
- в данном исполняемом файле у функции нет инструкции
ret
Почему компилятор сгенерировал функцию, у которой нет инструкции возврата? Ведь в том же С/С++ у функции всегда есть оператор возврата, даже если он не прописан явно. Для меня (ничего не понимающего в компиляторах, и не являющегося одним из тех единиц на планете, кто понимает UB) это выглядит как баг компилятора. Просто глядя на результат.
Почему компилятор сгенерировал функцию, у которой нет инструкции возврата?
Компилятор - он про эквивалентные преобразования(*). Если в ф-ии нет возврата - значит и не задумывалось программистом и позволяется языком (LLVM - он бэкэнд ко многим языкам).
*) За исключением концепции UB - когда преобразования могут быть не эквивалентными. Поэтому и вопросы не к компилятору, а к UB.
void foo(int *ptr) {
printf("Before the loop\n");
for (int i = 0; i < 1000; i++) {
printf("Before check\n");
if (i == 17)
*ptr = 1;
printf("After check\n");
}
printf("After the loop the loop\n");
}
* Условие
i == 17
станет истинным (причём только 1 раз) в ходе выполнения данного цикла;* Поэтому запись единицы в
*ptr
обязательно произойдёт;* Чтобы не делать лишнюю проверку в цикле, компилятор имеет право сделать любое из следующих преобразований:
void foo(int *ptr) {
*ptr = 1;
printf("Before the loop\n");
for (int i = 0; i < 1000; i++) {
printf("Before check\n");
printf("After check\n");
}
printf("After the loop the loop\n");
}
Это какое-то слишком сильное упрощение. Не имея определения функции printf компилятор не может этого сделать, как минимум по двум причинам:
у printf может быть внутреннее состояние, которое заставит её сделать нелокальный переход на какой-нибудь итерации цикла в функции foo. Если это случится до 17й итерации, то присваивание *ptr до начала выполнения цикла может изменить видимое поведение программы.
printf может обращаться к объекту, на который указывает ptr переданный в foo. Если значение объекта изменится не ровно между двумя вызовами printf на 17й итерации, то это может изменить видимое поведение программы.
Да, хорошее замечание, но я опустил эти подробности, имея в виду, что читатель все-таки знает, что такое printf. На самом деле достаточно вывести willreturn для этой функции (тогда не будет зависания или передачи управления неизвестно куда) и noalias для ptr. Полное определение тут можно не иметь, хватит атрибутов.
Создается впечатление, что UB в их текущем виде и в контексте оптимизирующих компиляторов являются исторически сложившейся утечкой абстракций из компиляторов в терминологию самого языка.
Я тоже такого мнения.
Вон, в Rust, к примеру, UB без unsafe нету. Там догадались не делать переполнение операций с целыми числами UB, как это делает C++. А это значительный процент случаев с UB. Есть ещё деление, но оно в Rust всегда с проверкой на 0 (что несколько медленно). Ну а всякие UB при чтении/записи памяти вообще устранены механизмами языка.
Там догадались не делать переполнение операций с целыми числами UB, как это делает C++.
Эта проверка только в dev
Там вообще все чуть сложнее. Да, в dev сборках по умолчанию включены проверки на переполнение, и вызов паники. В релизных сборках поведение можно контролировать через функции checked_*, или wrapped_*. Если сделать обычным оператором, и число переполнится, то результат не определен, но есть нюанс
Компилятор раста в своих оптимизациях не имеет права полагаться на то, что знакового переполнения в программе нет
Поведение у раста в таком случае определено — он выдаст инструкцию add (без nsw флага для llvm). А вот результат выполнения этой инструкции — как процессор посчитает, так и будет, хз
Поведение у раста в таком случае определено — он выдаст инструкцию add (без nsw флага для llvm). А вот результат выполнения этой инструкции — как процессор посчитает, так и будет
И это выглядит (для человека, ничего не понимающего в UB и в компиляторах) со всех сторон лучше и адекватнее.
Вопрос — почему в С++ в этой конкретной ситуации не сделали так же?
Легаси. Сначала обратная совместимость с Си, потом с самим собой прошлых версий
А когда писался Си, оптимизирующих компиляторов ещё не было. Они появились потом, когда оказалось, что транслировать напрямую код под разные архитектуры получается не очень эффективно. Ну и опять же, программы были меньше, помещались в голове проще, избегать UB тоже было проще
Сейчас программы стали больше и сложнее, и поэтому появляются языки с более удобными инструментами для управления этой сложностью, типа того же раста
Не вполне понял. При чём тут совместимость с Си, если во времена Си самого понятия UB ещё не было, и результат фактической компиляции (и выполнения) скорее всего соответствовал тому, как это сейчас в Расте.
Ага, там отличается поведение в отладочной сборке и в релизе. Однако, это всего лишь два возможных варианта поведения, а не бесконечные возможности UB.
Java это сделала задолго до Раста. Это не "додумались убрать UB, которое исторически откуда-то притекло", а "вполне осознанно зарезали оптимизации путём спецификации любого поведения в угоду безопасности программирования". Перфоманс соответствующий.
Disclaimer. Я вовсе не фанат раста, там тоже куча проблем совершенно другого характера, но с другой стороны, конкретно такого ужаса, как UB в целочисленной арифметике, всё же нет.
У джавы в числодробилках (т.е. когда в бенче не создаются объекты, не выделяется активно память и не собирается gc и т.п.) нет никаких других дополнительных проблем. Перфоманс у неё будет плюс-минус как у раста (если речь о Зинге используется одна версия LLVM)
Примеры на расте строятся банально (достаточно заюзать что угодно с UB). Ну например:
С++: https://godbolt.org/z/z6dxvrE13
Rust: https://godbolt.org/z/nK19aWMYE
Проверки на 0 честно делаются и никуда не выкашиваются, поэтому в цикле сложный CFG, против одного блока в С++. Я это не бенчмаркал, потому что мне лень, но перфоманс тут и так понятен.
Я бы сказал, что сложно построить пример, где возможны в теории (но не происходят на практике) какие-то исключительные ситуации (которые в С++ ведут к UB, а в rust -- к panic), и при этом перфоманс будет одинаковый. Если сможете это сделать, можете покидать примеры, очень интересно.
godbolt.org/z/z6dxvrE13Этот ваш <вырезано цензурой> сайт мало того, что требует javascript просто для чтения, так ещё и firefox требует аж 91 версии, даже просто quantum и даже 78 ему мало. Вы бы ещё этот пример в сматрфонное приложение загнали, причём не запускающееся без какого-нибудь google authenticity test и проверки на отсутствие root.
По делу. Если в расте генерацию отдельной проверки знаменателя на 0, действительно никак нельзя отключить, то это плохо, конечно. Растовскому компилятору точно нельзя сказать «когда видишь оператор
/
, генерируй без всяких проверок процессорную инструкцию деления, а если она вернёт SIGFPE, то пусть так и будет, только больше ничего другого менять не надо»? Или у процессора тоже бывает UB на ассемблерном уровне, и «быстрая» инструкция деления, которая его создаёт, если в знаменателе 0 (в отличие от медленной, которая в этом случае обязательно кидает SIGFPE)?Потому что если до «UB на уровне процессора» дело пока не дошло, то получается, что процессор имеет отличный (ну или приемлемый: не всегда получение в программе сигнала полезно, но во многих случаях приемлемо) быстрый способ за одну инструкцию и проверить знаменатель на 0, и поделить, а и rust, и c++, несмотря на свою заявленную «близость к железу» до этого быстрого способа добраться не позволяют.
Деление на 0 это ерунда. Вот наслаждайтесь: https://godbolt.org/z/85o7xhjxe
godbolt.org/z/85o7xhjxeО, нет, и вы туда же. Я ещё понимаю, что этот 91-й сайт использует топикстартер, который топит за UB. Но вы-то вроде как против? И даже критикуете обновления компилятора за деградацию (с точки зрения программиста). А для примеров всё равно 91-й сайт используете.
По делу. Я так понимаю, «poison» внутри llvm-функции main — это не перенос имени переменной из исходника, а ключевое слово этого языка llvm. Но я не очень знаю особенности работы языка llvm, поэтому не очень понимаю, что будет, если этот пример дальше скомпилировать в реальный машинный код? Будет возвращаться неинициализированное значение? Или опять инструкцию ret снесут целиком, передав управление дальше по памяти (вот теперь уже точно с непредсказуемыми последствиями, поскольку функция последняя)?
(И да, в любом случае, к «близости к железу» и «использованию возможностей процессора так, как этого хочет программист» это отношения не имеет, независимо от ответа.)
Раньше было веселей, когда не было виртуальной защищённой памяти. Можно было скомпилировать вообще любой код или текст.
Можно было переименовать текстовый файл как exe и смотреть на сложное поведение и зарождение ии. С рандомными указателями было самое веселое.
Чем больше проверок и защиты - тем больше будет багов.
У меня в практике был такой случай. Довольно большой embedded-проект (ARM архитектура, если имеет значение), код местами бывает странный. Обнаружилось интересное-невероятное. Допустим, есть код функций int A() {...} и B() {...} (сигнатура B вообще не важна, важно, что в исходнике они идут подряд, ровно одна за другой). Если в конце A() не прописать return со значением, то... управление переходит в функцию B! (upd: если быть точным, то не "переходит", конечно, а просто ret отсутствует) Вот это было сильно неочевидно, и, собственно, после этого мы включили необходимые предупреждения. Компилятор gcc 10.3.1.
И в этом смысле обвинения в UB за integer overflow (особенно вместе с integer promotion или как это называется) — куда большая проблема и опасность, чем, скажем, деление на нуль или нулевые указатели. В случае деления на нуль можно написать что-то вроде
if (x == 0) exit (1);
z = y / x;
(хотя лучше бы, конечно, компилятор не заставлял меня замусоривать код, а вместо этого нормально отдал бы процессору инструкцию деления и позволил бы ему выдать SIGFPE, но уж что есть). В случае нулевого указателя, опять же, можно написатьif (ptr == 0) exit(1);
*ptr = 1;
(хотя указатели создают куда больше опасностей, чем разыменование нулевого, если нужна реальная защита от обвинений в UB, лучше вообще использовать глобальные переменные для больших объёмов данных вместо передачи в функции указателей).Но с integer overflow проблема в том, что не сложив (или не перемножив) два числа, вообще невозможно понять, будет там переполнение или нет. (Кстати, в следующем могу ошибаться, но если мне не изменяет память, то intel, узнав о «прогрессе» концепции UB в C++, несмотря на все заверения в обратной совместимости, в какой-то версии процессора удалил поддержку инструкции, которая при integer overflow не то кидала сигнал, не то устанавливала флаг по типу таких, что устанавливает инструкция cmp — так что теперь ещё труднее проверить.) Фактически, для защиты от обвинений в UB приходится полностью избегать знаковых целочисленных операций, кроме, возможно, деления. Приходится писать что-то типа
(int) (((unsigned long long) a) + ((unsigned long long) b))
(причём из-за promotion даже просто unsigned написать нельзя), и то, это гарантирует только отсутствие UB, а не знание о том, будет переполнение или нет.Конкретный вопрос 1, напрямую связанный с promotion. Писать long long в долгосрочной перспективе тоже не вполне безопасно — что если выйдет версия компилятора, в которой можно писать три и больше long подряд, а unsigned long long будет объявлен «немаксимальным» и, следовательно, подлежащим promotion (читай — UB)? Вопрос: можно ли на уровне компилятора и стандарта определить «максимальный беззнаковый целочисленный тип», чтобы он гарантированно не лез туда со своим promotion в будущем?
Конкретный вопрос 2: Реализовано ли уже что-то вроде «UB-безопасной библиотеки целочисленной арифметики»? Чтобы можно было писать что-то вроде
#include <ub_safe_integers.h>
...
UBSafeInt32 a = 1000000000, b = 2000000000, c = 1, d = 0; // это вызывает конструктор
printf ("%d\n", (a + b).convert_to_official_int_for_percent_d ()); // _for_percent_d - специально, чтобы напомнить, что кроме как для окончательного вывода, конвертировать в официальные инты опастно
UBSafeInt32::set_perror_on_division_by_zero (1);
printf ("%d\n", (с / d).convert_to_official_int_for_percent_d ()); // выводит сообщение об ошибке и делает exit(1), предшествующую операцию сложения не ломает
, а под капотом (с помощью переопределения операторов) было бы то самое приведение типов с защитой от promotion и проверкой на нуль. В идеале ещё и оптимизатор бы генерировал на такое сложение ровно одну инструкцию вида «сложить два числа с помощью 2-complement и не выпендриваться», но от авторов концепции UB ждать подобного по меньшей мере наивно.Приходиться флагами компилятора (типа -fwrapv) отключать весь этот угар. Что бы пользоваться нормальной модульной арифметикой. А вообще если программа делает вообще не то после оптимизации, уже явный индикатор неадекватности оптимизаций. Но нет с точки зрения компилятора всё ок. Значит проблема в этой самой точке зрения.
ps: а что происходит при делении на 0.0 с плавающей точкой ?
а что происходит при делении на 0.0 с плавающей точкой ?Не знаю точно, если честно. Чисто умозрительно могу предположить, что либо тоже UB, либо IEEE удалось всё же прогнуть авторов стандарта, и тогда либо спецзначение NaN (not a number), либо спецзначение Inf (infinity), возможно, с плюсом или минусом.
Э-э-э, а что случилось с инструкциями INTO и JO? Или вы имели в виду какие-то другие?
Я помню, что когда детально изучал этот вопрос (когда мне нужно было сложное математическое вычисление, в котором действительно могло быть переполнение, и нужно было это переполнение обнаружить, не слишком замедляя вычисления), я находил какую-то исчезнувшую инструкцию и даже критику intel со стороны программистов, что инструкцию убрали. Сейчас за пару часов не нашёл, хотя про INTO пишут, что в 64-битном режиме она не поддерживается. Если JO всё равно существует, то единственное, что приходит в голову (с учётом требований не слишком замедлять) — что была какая-то одна инструкция, которая сразу либо делала сложение-умножение, либо кидала сигнал (или так вела себя любая арифметическая инструкция после установки какого-то режима).
А может быть, мои воспоминания касаются именно инструкции INTO, и это она действительно исчезла в 64-битном режиме, хотя при отсутствии переполнений одна дополнительная инструкция JNO на каждое умножение вряд ли была бы сильно медленее одной инструкции INTO — всё равно инструкции две — арифметическая и эта (INTO/JNO).
Если бы мы захотели иметь такой warning, он бы вылетал примерно на каждой первой строчке вашей программы.Это всё же некоторое передёргивание. Я понимаю, что для авторов идеи UB выдавать warning на каждую арифметическую операцию со знаковыми (после promotion) целыми числами было бы всё же слишком палевно, они на это не пойдут. Сликом открыто была бы видна их злобная сущность. Примерно как авторы законов о money laundering, тем не менее, не решились до сих пор открыто изъять все деньги со всех банковских счетов населения.
Но вот ситуация «я, компилятор, на какой-то кривой козе „пришёл к выводу“, что этот цикл на самом деле бесконечный, поэтому удаляю его из кода полностью» происходит совсем не так часто, warning можно было бы выдать. А дальше уж пусть программист ищет, где он в этом цикле пропустил приведение типа к unsigned long long, чтобы прекратить выпендрёж компилятора до того, как программа будет запущена.
И уж тем более можно было бы выдать warning при удалении инструкции ret в конце функции. Хотя вот это на самом деле уже полное свинство. У «бесконечных» циклов ещё хоть какая-то отмазка есть, типа их придётся потом дальше оптимизировать (хотя казалось бы, можно просто пометить код внутри этого цикла как «дальше оптимизировать не надо» — и компилятор не будет делать «лишней», с точки зрения своей искривлённой реальности, работы, и программист получит что-то, чуть более напоминающее исходно написанную программу). Но одна инструкция ret? Кому она там мешала? А ведь передача управления в совершенно ненужную в данный момент функцию — это ещё не самое страшное. А если функция с выкинутым ret окажется последней в итоговом исполняемом файле? Что дальше? Процессор неинициализированную память как команды начнёт выполнять? И к чему это приведёт?
Да, если сигнатура функции предполагает возврат значения, может быть уже не так просто, компилятор может решить, что ему неоткуда брать это значение. Но и в этом случае есть куча решений для компилятора, куда менее пагубных, чем передача управления неизвестно куда.
1. Забивка нулями той памяти/регистра, откуда функция уровнем выше будет читать возвращаемое значение. Минусы: какой-то код добавить надо, больше одной инструкции; если в программе действительно ошибка (то есть если реальная программа, а не только UB-представление компилятора о ней, доходит до последней строчки функции, не встречая оператора return, а программист этого не планировал), то когда из-за этих неожиданных нулей что-то пойдёт не так дальше, восстановить причину при отладке будет сложнее.
2. Сдвиг вершины стека (если возвращаемое значение передаётся через стек) так, чтобы прочитать возвращаемое значение было можно, и вершина стека вернулась бы после этого куда надо, но память по этому адресу оставить неинициализированной. Минусы: если в программе действительно ошибка, как в предыдущем пункте, то ровно те же минусы, как чтение из неинициализированной памяти, «плавающие баги». Вероятно, самое плохое решение, кроме, собственно, передачи управления за пределы функции.
3. Ошибка периода выполнения. Минусы: тоже нужно добавлять машинный код, который будет лишним, если программист, в отличие от тупого компилятора, таки может доказать, что до последней строчки этой функции управление не добирается; а если в этом случае управление добирается, но возвращаемое значение никак не используется, то получаем совершенно ненужное программисту падение программы, которая без этого работала бы нормально.
4. Ошибка периода компиляции. Минусы: в ситуации, когда опять же, программист, в отличие от тупого компилятора, таки может доказать, что до последней строчки этой функции управление не добирается, получаем замусоривание исходника генерацией валидного возвращаемого значения.
И конечно, при использовании решений 1-3 тоже можно при желании показать warning.
(Примечание. Если в реальном исходнике последний оператор в функции — что-то типа return 0;, а компилятор из-за каких-то «рассуждений об UB» это игнорирует, то это не «неоткуда брать значение», это «компилятор оборзел сверх меры». В таком случае предлагаемые выше решения действительно не имеют смысла, а смысл имеет завершение машинного кода функции возвратом этого самого нуля, что бы там компилятор ни думал себе об UB.)
Я понимаю, что для авторов идеи UB выдавать warning на каждую арифметическую операцию со знаковыми (после promotion) целыми числами было бы всё же слишком палевно, они на это не пойдут. Сликом открыто была бы видна их злобная сущность. Примерно как авторы законов о money laundering, тем не менее, не решились до сих пор открыто изъять все деньги со всех банковских счетов населения.
Я всё жду, когда авторов идеи UB начнут сравнивать с нацистами или даже конкретно с Гитлером. :)
Давайте я сразу пошлю в лес все инсинуации на тему "чего можно было бы сделать". clang и LLVM -- открытые продукты. Вы можете пойти и написать соответствующие ворнинги там, где хотите. Если вы сможете обосновать ревьюерам их необходимость, ваши правки примут. Честно. Никакой магии нет.
Я рассказываю о том, как оно работает сейчас, и объясняю, почему. Это не значит, что это лучший или единственно возможный способ делать то или это, но жизнь такова и более никакова. Вы можете 125 раз быть несогласны со вторым началом термодинамики или с правилами навигации кораблей в полярных водах, но разглагольствовать об этом здесь -- бесполезно.
Я всё жду, когда авторов идеи UB начнут сравнивать с нацистами или даже конкретно с Гитлером. :)Не передёргивайте. В мире много зла помимо Гитлера, координально отличающегося от Гитлера, и даже вряд ли сравнимого «абстрактно и вообще» по уровню вреда с Гитлером. Отличие концепции UB от конкретно Гитлера совершенно не позволяет сказать, что в ней нет зла.
Если вы сможете обосновать ревьюерам их необходимость, ваши правки примут. Честно.Вы сами-то верите в эту идею? Она очевидно бесполезна. По таким громким и чувствительным вопросам, как UB, авторы компиляторов ни за что не пойдут на уступки как минимум без сильной предварительной шумихи на форумах.
Я рассказываю о том, как оно работает сейчас, и объясняю, почему.
Вы можете 125 раз быть несогласны со вторым началом термодинамикиВот это как раз отличный пример фанатизма разработчиков компиляторов в деле продвижения UB. Ещё раз. Если компилятор творит хрень и портит мои данные непредсказуемым образом, мне не важно почему он это делает. Мне важно, чтобы он прекратил это делать.
А ваши рассуждения выглядят как демагогия. Не надо приравнивать законы природы, вроде начал термодинамики, с кривыми человеческими решениями, особенно принимаемыми сравнительно небольшими коллективами. Во втором случае огласка и шумиха как раз часто оказывается полезной. Уж всяко более полезной, чем какие-то бюрократические методы, особенно когда у бюрократов, принимающих решения, даже формально нет никакой обязанности его как-то обосновывать.
Если компилятор творит хрень и портит мои данные непредсказуемым образом, мне не важно почему он это делает. Мне важно, чтобы он прекратил это делать.
Прикол в том что старый компилятор компилил код который работал, а вот свежий, модный, молодёжный как раз компилит хрень, и тут мы начинаем поиск UB, который заложили те кто списал кодовую базу до вас. Вот где веселье-то зарыто.
signal (SIGSEGV, exit_gracefully);
и хочу проверить, что обработка SIGSEGV действительно работает как задумано. Я надеюсь, что моя программа не делает SIGSEGV. С одной стороны, точной уверенности у меня нет, но с другой стороны, «нормальным» способом, «как есть», я специально загонять программу в SIGSEGV не умею. Вопрос: могу ли я что-то сказать компилятору, чтобы он ровно в нужный мне момент (когда все инициализации уже проведены, и. т. д.) сгенерировал инструкцию «прочитать память по нулевому адресу»? Чтобы выполнилась именно эта инструкция, и процессор послал сигнал SIGSEGV, и управление передалось функции exit_gracefully, а не «что угодно, потому что читать по нулевому адресу — это UB»?«нормальным» способом, «как есть», я специально загонять программу в SIGSEGV не умею
kill -11?
могу ли я что-то сказать компилятору, чтобы он ровно в нужный мне момент (когда все инициализации уже проведены, и. т. д.) сгенерировал инструкцию «прочитать память по нулевому адресу»?
По идее, если мы хотим сказать компилятору "сгенерируй инструкцию, которая с точки зрения твоей виртуальной машины невозможна", вариант всегда один - ассемблерные вставки.
kill -11?Хорошо, под «нормальным способом» я имел в виду «изнутри программы». Другими словами — как дать программе такой ввод, чтобы произошло чтение по нулевому адресу. Но да, для решения этой конкретной задачи kill -11 подходит.
«сгенерируй инструкцию, которая с точки зрения твоей виртуальной машины невозможна», вариант всегда один — ассемблерные вставки.Ассемблерная вставка — это, конечно, решение, но всё равно в таком «ограничении виртуальной машины» я ничего особо хорошего не вижу. То есть другие решения, конечно, существуют, но на мой взгляд, лучше бы, чтобы компилятор не пытался «защищать» программиста избыточно (избыточно — с точки зрения программиста в данном конкретном случае, о которой он чётко и недвусмысленно сообщил компилятору).
Пожалуйста, перестаньте писать мне, что "курощение" -- это опечатка. Это не опечатка, а отсылка к Карлсону. Стыдно классику не знать, товарищи! :)
В любой момент компилятор имеет право превратить poison в любое значение, какое ему нравится.
Мне кажется, здесь требуется одно очень существенное уточнение. Если я не ошибаюсь, то не только имеет право превратить в любое значение, но может превращать его в любое количество различных значений даже если все они подряд в одном выражении стоят. Без этого понять, например
bool question() { bool tobe; return tobe || !tobe; }
совершенно невозможно
Как же уморительно читать кучу комментариев о том, как злой компилятор сломал всем их уютненькие багованные программы. Если так нужно, чтоб компилятор "делал что написато и не умничал", передавали бы ему -O0, и всего делов. Но кому же захочется чтоб тормозил только его код, гораздо комфортнее, когда даже на -O3 тормозит сразу у всех.
Корректный код не должен работать медленно из-за того, что где-то кто-то продолжает писать с ошибками. Change my mind.
Корректный код не должен работать медленно из-за того, что где-то кто-то продолжает писать с ошибками
Вы просто еще не поняли что не бывает корректного кода собранного из сотен библиотек и написанного кучей народу. В то время как наличие в любом месте этого кода UB даёт компилятуру индульгенцию на любую дичь.
Поговорим об оптимизирующих компиляторах. Сказ третий: неопределённое поведение и оптимизации