Неопределённое поведение (, в ряде источников непредсказуемое поведение) — ситуация, когда в определённых маргинальных случаях поведение программного продукта или устройства может меняться неконтролируемым образом и приводить к некорректным результатам, но это не является ошибкой, и о такой возможности указано в спецификации. Как правило, предполагается, что пользователь данного продукта имеет достаточную компетенцию, чтобы избежать этих случаев. Чаще всего речь идёт о неопределённом поведении в языках программирования.
Неопределенное поведение не следует путать с неуточняемым поведением (), при котором спецификация разрешает не любое поведение, а только ограниченный диапазон вариантов реализации.
Причины Основные причины, по которым может допускаться неопределённое поведение: Технические ограничения; Оптимизации с целью снижения расходов на производство, эксплуатацию, повышение быстродействия и снижение потребления ресурсов; Возможность нескольких реализаций, например — нескольких разных компиляторов от разных производителей по одной спецификации языка программирования; Работа на разных платформах со своими техническими ограничениями — уровень кроссплатформенности может разниться от «на этой платформе тоже есть такой язык» до «если обходить крайние случаи, будет работать одинаково»; Возможность изменения продукта со временем. Пример: платформа x86 разрешает доступ по невыровненным адресам, но как это будет видеть посторонний процесс — не определяется. В качестве примера можно привести арифметические операции с переполнением. Например, необходимо вычислить в целых числах (2-байтовых знаковых, …) значение
При небольших значениях a и b пример всегда решается корректно. Но если превысит максимальный для данной платформы результат, возникнет неопределённость: В зависимости от платформы, арифметическое переполнение может привести к: аварийному останову (перехватываемому или не перехватываемому программистом); упору в край — независимо от слагаемых, на 16-битной системе вернёт результат , и итогом будет ; но чаще произойдёт циклическое переполнение: если равно , а равно , в итоге получится вместо ожидаемого . Кроме того, компилятор может обойти ошибку и выдать правильный ответ одним из способов: Компилятор может по каким-то причинам посчитать, что более оптимальным будет преобразовать выражение так, чтобы деление происходило раньше сложения, или каким-либо другим способом будет обойдён момент переполнения; Может задействовать регистр флагов или длинную арифметику для хранения промежуточного 17-го бита; На платформе с бо́льшим размером слова сумма уместится в регистр, и если результат деления умещается в исходные 16 битов, ответ будет верным.
Система может повести себя неопределённым образом в другом месте, даже до выполнения инструкций, его вызывающих. Оптимизирующий компилятор, исходя из знания об ограничениях платформы, может сделать вывод о максимально возможных значениях переменных, и выполнить оптимизации, которые могут работать некорректно при выходе этих переменных за данный диапазон — и неопределённое поведение может появиться вне выражения, собственно это неопределённое поведение вызвавшего. Например, если известно, что переменная всегда больше , компилятор может посчитать, что переменная всегда меньше . Если при этом в коде где-то есть проверка, больше ли чем , которая при этом не меняет переменных и и не влияет на вычисление выражения, компилятор может посчитать, что её результат всегда будет равен true, и исключить её, даже если эта проверка осуществляется до исполнения выражения, вызывающего переполнение.
Распространено жаргонное название последствий неопределённого поведения в C, как «носовых демонов», после того, как один из пользователей usenet объяснил UB как «если программа, содержащая UB, заставит демонов вылетать из вашего носа, это не будет нарушением спецификации».
Борьба с неопределённым поведением В случае языков программирования и программных библиотек, борьба с неопределённым поведением возложена на плечи программиста, их использующего. Часть проблем можно обнаружить, используя статический анализ кода, некоторые проблемы проявляются в предупреждениях компилятора. В каких-то случаях приходится дополнять программу проверками на значения, которые могут вызвать UB.
Некоторые языки программирования исключают неопределённое поведение, запрещая делать оптимизации, которые могли бы привести к UB и устанавливая дополнительные проверки выхода за границы. Также многие оптимизирующие компиляторы позволяют либо отключить подобные оптимизации, либо установить на опасные участки дополнительные проверки.
Примеры
В языке Си, к примеру, использование переменной до её инициализации приводит к неопределённому поведению. Согласно спецификации компилятор должен в этом случае сделать что-либо, что может показаться наиболее эффективным/простым. Неопределённое поведение возникает при попытке обращения к переменной.
Библиотеки могут не проверять указатели на NULL для быстродействия.
В процессорах x86, если есть два последовательных порта ввода-вывода и требуется записать информацию сначала в один порт, затем в другой — это следует делать по одному байту, поскольку порядок прихода байтов на оборудование не гарантируется.
Ещё один пример неопределенного поведения: курьёз с ANSI-директивой «#pragma». Согласно спецификации языка, компиляторам предоставлена полная свобода при обработке этой конструкции. До версии 1.17 компилятор GCC при нахождении в исходном коде этой директивы пытался запустить Emacs с игрой «Ханойские башни».
В качестве ещё одного примера неопределённого поведения можно привести код:
int i = 5; i = ++i + ++i;
При его выполнении переменная i может принять значения 13 или 14 для C/C++, 13 для Java, PHP и C#, 12 при реализации на LISP. Неопределенность в языках C и C++ связана с тем, что, согласно стандартам C и C++, побочные эффекты (то есть инкремент в данном случае) могут быть применены в любой удобный для компилятора момент между двумя точками следования.
Достоинства
Определение некоторых операций как «неопределённых» приводит подобные языки (характеризующиеся зачастую отсутствием встроенной проверки на предел массива и т. д.) к упрощению спецификации и некоторому увеличению гибкости. Ускоряется работа программ (так как не нужно проверять всевозможные «маргинальные» случаи).
Недостатки
Не гарантирует полной совместимости различных реализаций языка. Недопущение ситуаций неопределённого поведения остаётся за программистом.
Примечания
Ссылки Мобильность на уровне исходных текстов Разыменовывание нулевого указателя приводит к неопределённому поведению
Категория:Ошибки программирования Категория:Концепции языков программирования