Некоторые распространенные языки программирования, в первую очередь C и C++, имеют строгое понятие неопределенное поведение: когда вы пытаетесь выполнить определенные операции вне пределов их использования, это вызывает неопределенное поведение.
Если происходит неопределенное поведение, компилятору разрешается делать все, что он хочет (включая вообще ничего, «путешествия во времени» и т. д.).
Мой вопрос: почему существует понятие неопределенного поведения? Насколько я могу судить, огромное количество ошибок, программ, которые работают с одной версией компилятора, перестают работать над следующей и т. д., Можно было бы предотвратить, если бы вместо того, чтобы вызывать неопределенное поведение, использование операций, выходящих за рамки их предполагаемого использования, привело бы к а ошибка компиляции.
Почему это не так?
Из-за идеологии C. Очень гибкий и мощный, оставляя все в руках программистов.
Интересный доклад на тему Чендлера Каррута: Мусор на входе, мусор на выходе: споры о неопределенном поведении ...
«использование операций за пределами их предполагаемого использования вызовет ошибка компиляции» Большинство неопределенных поведений в C не обнаруживаются статически, поэтому они не могут быть ошибками компиляции. Это должны быть ошибки времени выполнения, что повлечет за собой затраты времени выполнения.
Несмотря на то, что тема интересная и важная, она слишком широка. Было проведено бесчисленное количество обсуждений и исследований, посвященных этому вопросу, и все же существуют языки, от отсутствия UB вообще до наличия UB повсюду (кхм, C / C++).
Неопределенное поведение может происходить из-за различий в платформах. Часто встроенным системам требуется доступ к функциям (через указатели), доступ к которым рабочие столы могут предотвратить. Также нет стандартной схемы размещения памяти для всех платформ. Небольшие встроенные системы не будут поддерживать тот же диапазон адресов настольных компьютеров или более мощных платформ.
UB существует, чтобы позволить системам платить только за то, что они используют; не нужно тратить ресурсы на профилактику (например, Java и C#). Например, во встроенной системе, которая не использует динамическое выделение памяти, запускать службу сборки мусора не нужно. Кроме того, для платформ, критичных по времени, случайная сборка мусора - это плохо.
@ThomasMatthews: Причины многих форм UB исторические. К сожалению, авторы компиляторов, которые не понимают разницы между «непереносимым» и «ошибочным» и которые считают «умный» и «глупый» антонимами, ухватились за него для гораздо более деструктивных целей.
Недавно вышла статья под названием Ценность неопределенного поведения, в которой приводятся хорошие примеры. Стоит проверить!
Я знаю, что немного опаздываю на вечеринку, но наиболее, если не все неопределенное поведение в C / C++, в общем случае не может быть обнаружено во время компиляции. Такие вещи, как, например, ошибки выхода за пределы массива или использование после освобождения.
@CoffeeTableEspresso Интересно то, что, например, Rust пытается отловить подобные ошибки. Конечно, семантика Rust не сравнима в соотношении 1: 1 с C / C++. Совершенно верно, что это невозможно для «общего случая», что означает, что (безопасный) Rust более консервативен / ограничен в том, как вы можете назначать / изменять память.
@Qqwy, поэтому я предпочитаю C / C++ / D Rust. Я бы предпочел, чтобы мой компилятор не был таким ограничительным, а просто использовал инструмент статического анализа для обнаружения любых ошибок. Вместо того, чтобы заставить мой компилятор мешать мне делать много правильных вещей, которые мощь являются ошибками.





Неопределенное поведение существует в основном для того, чтобы дать компилятору свободу оптимизации. Одна вещь, которую он позволяет компилятору, например, работать в предположении, что определенные вещи не могут произойти (без предварительного доказательства того, что они не могут произойти, что часто бывает очень сложно или невозможно). Позволив ему предположить, что определенные вещи не могут произойти, компилятор может затем выполнить код исключить / не создавать, который в противном случае потребовался бы для учета определенных возможностей.
Есть ли что-нибудь в Обосновании C89 или другой документации 1980-х годов, чтобы поддержать эту точку зрения, или это более современное изобретение?
Неопределенное поведение в основном зависит от цели, для которой оно предназначено. Компилятор не несет ответственности за динамическое поведение программы или статическое поведение в этом отношении. Проверки компилятора ограничены правилами языка, и некоторые современные компиляторы также выполняют некоторый уровень статического анализа.
Типичный пример - неинициализированные переменные. Он существует из-за синтаксических правил C, в которых переменная может быть объявлена без значения инициализации. Некоторые компиляторы присваивают таким переменным 0, а некоторые просто присваивают переменной указатель mem и так и остаются. если программа не инициализирует эти переменные, это приводит к неопределенному поведению.
Why does this notion of undefined behaviour exist?
Чтобы язык / библиотека могли быть реализованы на множестве различных компьютерных архитектур настолько эффективно, насколько это возможно (- и, возможно, в случае C - при сохранении простоты реализации).
if instead of causing undefined behaviour, using the operations outside of their intended use would cause a compilation error
В большинстве случаев неопределенного поведения невозможно - или слишком дорого в ресурсах - доказать, что неопределенное поведение существует во время компиляции для всех программ в целом.
Случаи Некоторый можно доказать для программ некоторый, но невозможно указать, какие из этих случаев являются исчерпывающими, и поэтому стандарт не пытается это сделать. Тем не менее, некоторые компиляторы достаточно умны, чтобы распознавать некоторые простые случаи UB, и эти компиляторы будут предупреждать программиста об этом. Пример:
int arr[10];
return arr[10];
Эта программа имеет неопределенное поведение. Конкретная версия GCC, которую я тестировал, показывает:
warning: array subscript 10 is above array bounds of 'int [10]' [-Warray-bounds]
Вряд ли стоит игнорировать подобное предупреждение.
Более типичной альтернативой неопределенному поведению было бы определение обработки ошибок в таких случаях, например, создание исключения (сравните, например, Java, где доступ к нулевой ссылке вызывает исключение типа java.lang.NullPointerException). Но проверка предварительных условий четко определенного поведения медленнее, чем отсутствие проверки.
Не проверяя предварительные условия, язык дает программисту возможность самостоятельно доказать правильность и тем самым избежать накладных расходов времени выполнения на проверку в программе, которая, как было доказано, не нуждается в этом. Действительно, эта сила связана с большой ответственностью.
В наши дни бремя доказательства четкости программы можно несколько облегчить, используя инструменты (пример), которые добавляют некоторые из этих проверок во время выполнения и аккуратно завершают программу при неудачной проверке.
Хорошо сказано! Эффективность платформы и переносимость кода сильно повлияли и сформировали C и C++. Некоторые улучшения C++ 11, такие как семантика перемещения, были направлены на устранение недостатка в потенциальной эффективности. Но трудно достичь и эффективности, и переносимости, не дав компиляторам большой свободы действий ... неопределенного поведения. Другие языки с менее неопределенным поведением могут быть менее производительными (иногда гораздо менее производительными). Это компромисс, и разные языки преследуют разные цели. Языки - это инструменты, подходящие в своей области.
Я бы дал +2, если бы мог. Я бы добавил, что новые (версии) языков пытаются минимизировать объем UB, добавляя более явные правила к языку (например, возьмите Rust и переместите семантику в C++ 11)
Другой распространенной альтернативой неопределенного поведения является указание результата, соответствующего естественному поведению многих целевых платформ. Например, Java указывает, что 65535 * 65537 будет обертываться таким образом, чтобы дать -1, а выражение сдвига, подобное 1 << 35, уменьшит величину сдвига по модулю 32 (давая 3) перед выполнением сдвига, что, таким образом, даст 8.
Классическим примером несогласованности платформ является выход за пределы допустимого диапазона: auto undef_shift(std::uint32_t v) { return v << 32; }. На некоторых архитектурах соответствующие команды левого сдвига перехватывают, на других он всегда возвращает ноль (так как это рассматривается как сдвиг всех битов), а на третьих он ведет себя так же, как return v;, потому что старшие биты второго операнда молча игнорируются (это относится к x86). Если бы они предписывали какое-либо конкретное поведение, все другие платформы были бы серьезно наказаны дополнительным кодом очистки / проверки, который компилятор должен был бы выдать.
@supercat Что касается мод-уменьшения операнда сдвига, если под общим вы имеете в виду x86, вы правы. ARM, с другой стороны, насытится нулями, и мне не нужно говорить вам, сколько миллиардов устройств ARM находится в обращении в настоящее время, через 23 года после создания Java. То же самое и для IA-64 (для операнда ширины сдвига регистра). Java ставит безопасность и переносимость выше максимальной производительности, что, безусловно, является правильным выбором - я просто говорю, что производительность - это точная причина, по которой C / C++ не пошел по этому пути.
@ArneVogel: Java определяет сокращение mod-32, независимо от архитектуры. Реализации Java на ARM должны добавлять операцию AND, если они не могут проверить, находится ли операнд в пределах 0..31. С другой стороны, указание языка, что результатом будет неопределенный выбор между x<<(y-1)<<1 и x<<(y & 31), позволило бы эффективно работать на многих платформах, в то же время позволяя (x<<y)|(x>>(32-y)) быть эффективным способом выполнения ротации.
Да, это то, что я написал (я рад, что мы согласны с этим): «Если бы они предписывали какое-либо конкретное поведение, все другие платформы были бы серьезно наказаны дополнительным кодом очистки / проверки, который компилятор должен был бы выдать». - Java на ARM не нарушает спецификации, но в целом код будет медленнее, чем возможно (если ширина сдвига не известна во время JIL). Уловка поворота проста, но я бы предпочел, чтобы у наконец-то была функция библиотеки поворота на C++. (Это можно легко реализовать как встроенный компилятор.)
@ArneVogel: Некоторые компиляторы ищут шаблон, который я показал для поворота, и заменяют его инструкцией поворота, а также варианты, меняющие местами левый и правый операнды или роли сдвигов влево и вправо. Это в значительной степени четыре простейших способа выполнить ротацию на платформе, когда x>>32 вернет либо 0, либо x (неважно, какой). Попытка избежать UB в этом случае не только требует использования более сложного выражения, но и становится гораздо менее очевидным, какие выражения компилятор должен искать и заменять вращением.
@supercat Хорошие моменты - я ответил в чате.
В значительной степени эта ссылка предназначена для UB: Что каждый программист на C должен знать о UB