Неопределённое поведение (англ. undefined behavior, в ряде источников непредсказуемое поведение[1][2]) — ситуация, когда в определённых маргинальных случаях поведение программного продукта или устройства может меняться неконтролируемым образом и приводить к некорректным результатам, но это не является ошибкой, и о такой возможности указано в спецификации. Как правило, предполагается, что пользователь данного продукта имеет достаточную компетенцию, чтобы избежать этих случаев. Чаще всего речь идёт о неопределённом поведении в языках программирования.
Неопределенное поведение не следует путать с неуточняемым поведением (unspecified behavior), при котором спецификация разрешает не любое поведение, а только ограниченный диапазон вариантов реализации.
Причины
правитьОсновные причины, по которым может допускаться неопределённое поведение:
- Технические ограничения;
- Оптимизации с целью снижения расходов на производство, эксплуатацию, повышение быстродействия и снижение потребления ресурсов;
- Возможность нескольких реализаций, например — нескольких разных компиляторов от разных производителей по одной спецификации языка программирования;
- Кроссплатформенность;
- Возможность изменения продукта со временем.
В качестве примера можно привести арифметические операции с переполнением. Например, необходимо вычислить в целых числах значение
При небольших значениях a и b пример всегда решается корректно. Но если превысит максимальный для данной платформы результат, возникнет неопределённость:
- Возможно, на данной конкретной платформе произойдёт исключение или даже аварийное завершение программы;
- Результат первого действия примет максимально возможное значение. К примеру, для 16-разрядной системы это будет 32 767, и итогом будет 16 383 (так как используется только целочисленное деление);
- Чаще всего происходит арифметическое переполнение, то есть если равно 17 000, а равно 18 000, в итоге получится −15 268 вместо ожидаемого 17 500.
- Компилятор может по каким-то причинам посчитать, что более оптимальным будет преобразовать выражение так, чтобы деление происходило раньше сложения, или каким-либо другим способом будет обойдён момент переполнения;
- Может использоваться длинная арифметика;
- Программа может быть перенесена на платформу с большим размером слова.
В последних трёх случаях будет корректный результат, однако только последние два можно считать корректными, так как переполнения строго говоря в них не происходит, и следовательно не происходит неопределённого поведения.
Также оптимизирующий компилятор исходя из знания об ограничениях платформы может сделать вывод о максимально возможных значениях переменных, и выполнить оптимизации, которые могут работать некорректно при выходе этих переменных за данный диапазон — и неопределённое поведение может появиться вне выражения, собственно это неопределённое поведение вызвавшего. Например, если известно, что переменная всегда больше 24 576, компилятор может посчитать, что переменная всегда меньше 8192. Если при этом в коде где-то есть проверка, больше ли чем 8192, которая при этом не меняет переменных и и не влияет на вычисление выражения, компилятор может посчитать, что её результат всегда будет равен true исключить её, даже если эта проверка осуществляется до исполнения выражения, вызывающего переполнение.
Распространено жаргонное название последствий неопределённого поведения в C, как «носовых демонов», после того, как один из пользователей usenet объяснил UB как «если программа, содержащая UB, заставит демонов вылетать из вашего носа, это не будет нарушением спецификации»[3].
Борьба с неопределённым поведением
правитьВ случае языков программирования и программных библиотек, борьба с неопределённым поведением возложена на плечи программиста, их использующего. Часть проблем можно обнаружить, используя статический анализ кода, некоторые проблемы проявляются в предупреждениях компилятора. В каких-то случаях приходится дополнять программу проверками на значения, которые могут вызвать UB.
Некоторые языки программирования исключают неопределённое поведение, запрещая делать оптимизации, которые могли бы привести к UB и устанавливая дополнительные проверки выхода за границы. Также многие оптимизирующие компиляторы позволяют либо отключить подобные оптимизации, либо установить на опасные участки дополнительные проверки.
Примеры
правитьВ языке Си, к примеру, использование переменной до её инициализации приводит к неопределённому поведению. Согласно спецификации компилятор должен в этом случае сделать что-либо, что может показаться наиболее эффективным/простым. Неопределённое поведение возникает при попытке обращения к переменной.
Библиотеки могут не проверять указатели на NULL для быстродействия.
В процессорах x86, если есть два последовательных порта ввода-вывода и требуется записать информацию сначала в один порт, затем в другой — это следует делать по одному байту, поскольку порядок прихода байтов на оборудование не гарантируется.
Ещё один пример неопределенного поведения: курьёз с ANSI-директивой «#pragma». Согласно спецификации языка, компиляторам предоставлена полная свобода при обработке этой конструкции. До версии 1.17 компилятор GCC при нахождении в исходном коде этой директивы пытался запустить Emacs с игрой «Ханойские башни».[4]
В качестве ещё одного примера неопределённого поведения можно привести код:
int i = 5;
i = ++i + ++i;
При его выполнении переменная i
может принять значения 13 или 14 для C/C++, 13 для Java, PHP и C#, 12 при реализации на LISP. Неопределенность в языках C и C++ связана с тем, что, согласно стандартам C и C++, побочные эффекты (то есть инкремент в данном случае) могут быть применены в любой удобный для компилятора момент между двумя точками следования.
Достоинства
править- Определение некоторых операций как «неопределённых» приводит подобные языки (характеризующиеся зачастую отсутствием встроенной проверки на предел массива и т. д.) к упрощению спецификации и некоторому увеличению гибкости.
- Ускоряется работа программ (так как не нужно проверять всевозможные «маргинальные» случаи).
Недостатки
править- Не гарантирует полной совместимости различных реализаций языка.
- Недопущение ситуаций неопределённого поведения остаётся за программистом.
Примечания
править- ↑ Программирование на языке C/C++. Самоучитель. — Dialektika, 2003-01-01. — 348 с. — ISBN 9785845904607.
- ↑ Павловская Татьяна Александровна. C/C++. Процедурное и объектно-ориентированное программирование. Учебник для вузов. Стандарт 3-го поколения. — "Издательский дом ""Питер""", 2014-07-30. — 496 с. — ISBN 9785496001090.
- ↑ nasal demons . Jargon File. Дата обращения: 10 сентября 2023. Архивировано 7 февраля 2013 года.
- ↑ A Pragmatic Decision | D-Mac's Stuff . Дата обращения: 21 марта 2009. Архивировано 1 июня 2009 года.
Ссылки
править- Мобильность на уровне исходных текстов Архивная копия от 11 октября 2006 на Wayback Machine
- Разыменовывание нулевого указателя приводит к неопределённому поведению Архивная копия от 2 апреля 2015 на Wayback Machine