Почему существует «неопределенное поведение»?

Некоторые распространенные языки программирования, в первую очередь C и C++, имеют строгое понятие неопределенное поведение: когда вы пытаетесь выполнить определенные операции вне пределов их использования, это вызывает неопределенное поведение.

Если происходит неопределенное поведение, компилятору разрешается делать все, что он хочет (включая вообще ничего, «путешествия во времени» и т. д.).

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

Почему это не так?

В значительной степени эта ссылка предназначена для UB: Что каждый программист на C должен знать о UB

Mike Vine 27.07.2018 14:25

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

0___________ 27.07.2018 14:25

Интересный доклад на тему Чендлера Каррута: Мусор на входе, мусор на выходе: споры о неопределенном поведении ...

Borgleader 27.07.2018 14:31

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

sepp2k 27.07.2018 14:32

Несмотря на то, что тема интересная и важная, она слишком широка. Было проведено бесчисленное количество обсуждений и исследований, посвященных этому вопросу, и все же существуют языки, от отсутствия UB вообще до наличия UB повсюду (кхм, C / C++).

Passer By 27.07.2018 15:19

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

Thomas Matthews 27.07.2018 16:23

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

Thomas Matthews 27.07.2018 16:27

@ThomasMatthews: Причины многих форм UB исторические. К сожалению, авторы компиляторов, которые не понимают разницы между «непереносимым» и «ошибочным» и которые считают «умный» и «глупый» антонимами, ухватились за него для гораздо более деструктивных целей.

supercat 28.07.2018 22:25

Недавно вышла статья под названием Ценность неопределенного поведения, в которой приводятся хорошие примеры. Стоит проверить!

Qqwy 15.08.2018 12:41

Я знаю, что немного опаздываю на вечеринку, но наиболее, если не все неопределенное поведение в C / C++, в общем случае не может быть обнаружено во время компиляции. Такие вещи, как, например, ошибки выхода за пределы массива или использование после освобождения.

CoffeeTableEspresso 29.06.2019 01:07

@CoffeeTableEspresso Интересно то, что, например, Rust пытается отловить подобные ошибки. Конечно, семантика Rust не сравнима в соотношении 1: 1 с C / C++. Совершенно верно, что это невозможно для «общего случая», что означает, что (безопасный) Rust более консервативен / ограничен в том, как вы можете назначать / изменять память.

Qqwy 30.06.2019 01:09

@Qqwy, поэтому я предпочитаю C / C++ / D Rust. Я бы предпочел, чтобы мой компилятор не был таким ограничительным, а просто использовал инструмент статического анализа для обнаружения любых ошибок. Вместо того, чтобы заставить мой компилятор мешать мне делать много правильных вещей, которые мощь являются ошибками.

CoffeeTableEspresso 30.06.2019 03:53
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
9
12
1 548
3

Ответы 3

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

Хороший разговор по теме

Есть ли что-нибудь в Обосновании C89 или другой документации 1980-х годов, чтобы поддержать эту точку зрения, или это более современное изобретение?

supercat 27.07.2018 19:11

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

Типичный пример - неинициализированные переменные. Он существует из-за синтаксических правил 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, такие как семантика перемещения, были направлены на устранение недостатка в потенциальной эффективности. Но трудно достичь и эффективности, и переносимости, не дав компиляторам большой свободы действий ... неопределенного поведения. Другие языки с менее неопределенным поведением могут быть менее производительными (иногда гораздо менее производительными). Это компромисс, и разные языки преследуют разные цели. Языки - это инструменты, подходящие в своей области.

Eljay 27.07.2018 14:44

Я бы дал +2, если бы мог. Я бы добавил, что новые (версии) языков пытаются минимизировать объем UB, добавляя более явные правила к языку (например, возьмите Rust и переместите семантику в C++ 11)

bartop 27.07.2018 15:05

Другой распространенной альтернативой неопределенного поведения является указание результата, соответствующего естественному поведению многих целевых платформ. Например, Java указывает, что 65535 * 65537 будет обертываться таким образом, чтобы дать -1, а выражение сдвига, подобное 1 << 35, уменьшит величину сдвига по модулю 32 (давая 3) перед выполнением сдвига, что, таким образом, даст 8.

supercat 27.07.2018 22:30

Классическим примером несогласованности платформ является выход за пределы допустимого диапазона: auto undef_shift(std::uint32_t v) { return v << 32; }. На некоторых архитектурах соответствующие команды левого сдвига перехватывают, на других он всегда возвращает ноль (так как это рассматривается как сдвиг всех битов), а на третьих он ведет себя так же, как return v;, потому что старшие биты второго операнда молча игнорируются (это относится к x86). Если бы они предписывали какое-либо конкретное поведение, все другие платформы были бы серьезно наказаны дополнительным кодом очистки / проверки, который компилятор должен был бы выдать.

Arne Vogel 28.07.2018 13:10

@supercat Что касается мод-уменьшения операнда сдвига, если под общим вы имеете в виду x86, вы правы. ARM, с другой стороны, насытится нулями, и мне не нужно говорить вам, сколько миллиардов устройств ARM находится в обращении в настоящее время, через 23 года после создания Java. То же самое и для IA-64 (для операнда ширины сдвига регистра). Java ставит безопасность и переносимость выше максимальной производительности, что, безусловно, является правильным выбором - я просто говорю, что производительность - это точная причина, по которой C / C++ не пошел по этому пути.

Arne Vogel 28.07.2018 13:30

@ArneVogel: Java определяет сокращение mod-32, независимо от архитектуры. Реализации Java на ARM должны добавлять операцию AND, если они не могут проверить, находится ли операнд в пределах 0..31. С другой стороны, указание языка, что результатом будет неопределенный выбор между x<<(y-1)<<1 и x<<(y & 31), позволило бы эффективно работать на многих платформах, в то же время позволяя (x<<y)|(x>>(32-y)) быть эффективным способом выполнения ротации.

supercat 28.07.2018 18:37

Да, это то, что я написал (я рад, что мы согласны с этим): «Если бы они предписывали какое-либо конкретное поведение, все другие платформы были бы серьезно наказаны дополнительным кодом очистки / проверки, который компилятор должен был бы выдать». - Java на ARM не нарушает спецификации, но в целом код будет медленнее, чем возможно (если ширина сдвига не известна во время JIL). Уловка поворота проста, но я бы предпочел, чтобы у наконец-то была функция библиотеки поворота на C++. (Это можно легко реализовать как встроенный компилятор.)

Arne Vogel 28.07.2018 20:01

@ArneVogel: Некоторые компиляторы ищут шаблон, который я показал для поворота, и заменяют его инструкцией поворота, а также варианты, меняющие местами левый и правый операнды или роли сдвигов влево и вправо. Это в значительной степени четыре простейших способа выполнить ротацию на платформе, когда x>>32 вернет либо 0, либо x (неважно, какой). Попытка избежать UB в этом случае не только требует использования более сложного выражения, но и становится гораздо менее очевидным, какие выражения компилятор должен искать и заменять вращением.

supercat 28.07.2018 22:24

@supercat Хорошие моменты - я ответил в чате.

Arne Vogel 30.07.2018 12:47

Другие вопросы по теме