Неопределённое поведение: различия между версиями

Материал из Википедии — свободной энциклопедии
Перейти к навигации Перейти к поиску
[непроверенная версия][отпатрулированная версия]
Содержимое удалено Содержимое добавлено
м r2.7.3) (бот добавил: zh:未定义行为
орфография
 
(не показаны 44 промежуточные версии 20 участников)
Строка 1: Строка 1:
{{не путать|неуточняемое поведение|неуточняемым поведением}}
{{не путать|неуточняемое поведение|неуточняемым поведением||Недокументированные возможности|недокументированными возможностями}}
'''Неопределённое поведение''' ({{lang-en|undefined behavior}}, в ряде источников ''непредсказуемое поведение''<ref>{{Книга|заглавие=Программирование на языке C/C++. Самоучитель|ссылка=https://books.google.com/books?id=LCCMvmnnicsC|издательство=Dialektika|год=2003-01-01|страниц=348|isbn=9785845904607}}</ref><ref>{{Книга|автор=Павловская Татьяна Александровна|заглавие=C/C++. Процедурное и объектно-ориентированное программирование. Учебник для вузов. Стандарт 3-го поколения|ссылка=https://books.google.com/books?id=UM1LDAAAQBAJ|издательство="Издательский дом ""Питер"""|год=2014-07-30|страниц=496|isbn=9785496001090}}</ref>) — ситуация, когда в определённых маргинальных случаях поведение программного продукта или устройства может меняться неконтролируемым образом и приводить к некорректным результатам, но это не является ошибкой, и о такой возможности указано в спецификации. Как правило, предполагается, что пользователь данного продукта имеет достаточную компетенцию, чтобы избежать этих случаев. Чаще всего речь идёт о неопределённом поведении в языках программирования.
'''Неопределённое поведение''' ({{lang-en|undefined behaviour}}) — свойство некоторых [[язык программирования|языков программирования]] (наиболее заметно в [[Си (язык программирования)|Си]]), [[библиотека подпрограмм|программных библиотек]] и [[аппаратное обеспечение|аппаратного обеспечения]] в определённых маргинальных ситуациях выдавать результат, зависящий от реализации компилятора (библиотеки, микросхемы) и случайных параметров наподобие состояния памяти или сработавшего [[прерывание (информатика)|прерывания]]. Другими словами, [[спецификация]] не определяет поведение языка (библиотеки, микросхемы) в любых возможных ситуациях, а говорит: «при условии А результат операции Б не определён». Допускать такую ситуацию в программе считается ошибкой; даже если на некотором компиляторе программа успешно выполняется, она не будет [[кроссплатформенность|кроссплатформенной]] и может отказать на другой машине, в другой ОС или при других настройках компилятора.


Неопределенное поведение не следует путать с [[неуточняемое поведение|неуточняемым поведением]] ({{lang-en2|unspecified behavior}}), при котором спецификация разрешает не любое поведение, а только ограниченный диапазон вариантов реализации.
Неопределенное поведение не следует путать с [[неуточняемое поведение|неуточняемым поведением]] ({{lang-en2|unspecified behavior}}), при котором спецификация разрешает не любое поведение, а только ограниченный диапазон вариантов реализации.

== Причины ==
Основные причины, по которым может допускаться неопределённое поведение:
* Технические ограничения;
* Оптимизации с целью снижения расходов на производство, эксплуатацию, повышение быстродействия и снижение потребления ресурсов;
* Возможность нескольких реализаций, например — нескольких разных компиляторов от разных производителей по одной спецификации языка программирования;
* Кроссплатформенность;
* Возможность изменения продукта со временем.
В качестве примера можно привести арифметические операции с переполнением. Например, необходимо вычислить в целых числах значение
:<math>c=\frac{a+b}{2}</math>
При небольших значениях a и b пример всегда решается корректно. Но если <math>a+b</math> превысит максимальный для данной платформы результат, возникнет неопределённость:
* Возможно, на данной конкретной платформе произойдёт исключение или даже аварийное завершение программы;
* Результат первого действия примет максимально возможное значение. К примеру, для 16-разрядной системы это будет {{число|32767}}, и итогом будет {{число|16383}} (так как используется только целочисленное деление);
* Чаще всего происходит [[арифметическое переполнение]], то есть если <math>a</math> равно {{число|17000}}, а <math>b</math> равно {{число|18000}}, в итоге получится {{число|-15 268}} вместо ожидаемого {{число|17500}}.
* Компилятор может по каким-то причинам посчитать, что более оптимальным будет преобразовать выражение так, чтобы деление происходило раньше сложения, или каким-либо другим способом будет обойдён момент переполнения;
* Может использоваться [[длинная арифметика]];
* Программа может быть перенесена на платформу с большим размером [[машинное слово|слова]].
В последних трёх случаях будет корректный результат, однако только последние два можно считать корректными, так как переполнения строго говоря в них не происходит, и следовательно не происходит неопределённого поведения.

Также [[оптимизирующий компилятор]] исходя из знания об ограничениях платформы может сделать вывод о максимально возможных значениях переменных, и выполнить оптимизации, которые могут работать некорректно при выходе этих переменных за данный диапазон — и неопределённое поведение может появиться вне выражения, собственно это неопределённое поведение вызвавшего. Например, если известно, что переменная <math>a</math> всегда больше {{число|24576}}, компилятор может посчитать, что переменная <math>b</math> всегда меньше {{число|8192}}. Если при этом в коде где-то есть проверка, больше ли <math>b</math> чем {{число|8192}}, которая при этом не меняет переменных <math>a</math> и <math>b</math> и не влияет на вычисление выражения, компилятор может посчитать, что её результат всегда будет равен true исключить её, даже если эта проверка осуществляется до исполнения выражения, вызывающего переполнение.

Распространено жаргонное название последствий неопределённого поведения в C, как «носовых демонов», после того, как один из пользователей [[usenet]] объяснил UB как «если программа, содержащая UB, заставит демонов вылетать из вашего носа, это не будет нарушением спецификации»<ref>{{cite web|title=nasal demons|url=http://www.catb.org/jargon/html/N/nasal-demons.html|website=[[Jargon File]]|access-date=2023-09-10|archive-date=2013-02-07|archive-url=https://web.archive.org/web/20130207030919/http://www.catb.org/jargon/html/N/nasal-demons.html|deadlink=no}}</ref>.

== Борьба с неопределённым поведением ==
В случае языков программирования и программных библиотек, борьба с неопределённым поведением возложена на плечи программиста, их использующего. Часть проблем можно обнаружить, используя [[статический анализ кода]], некоторые проблемы проявляются в предупреждениях компилятора. В каких-то случаях приходится дополнять программу проверками на значения, которые могут вызвать UB.

Некоторые языки программирования исключают неопределённое поведение, запрещая делать оптимизации, которые могли бы привести к UB и устанавливая дополнительные проверки выхода за границы. Также многие оптимизирующие компиляторы позволяют либо отключить подобные оптимизации, либо установить на опасные участки дополнительные проверки.


== Примеры ==
== Примеры ==
Строка 8: Строка 35:
В языке Си, к примеру, использование переменной до её инициализации приводит к неопределённому поведению. Согласно спецификации компилятор должен в этом случае сделать что-либо, что может показаться наиболее эффективным/простым. Неопределённое поведение возникает при попытке обращения к переменной.
В языке Си, к примеру, использование переменной до её инициализации приводит к неопределённому поведению. Согласно спецификации компилятор должен в этом случае сделать что-либо, что может показаться наиболее эффективным/простым. Неопределённое поведение возникает при попытке обращения к переменной.


Большинство библиотек не проверяют указатели на [[NULL (Си)|NULL]].
[[Библиотека (программирование)|Библиотеки]] могут не проверять указатели на [[NULL (Си)|NULL]] для быстродействия.


В процессорах [[x86]], если есть два последовательных [[порт ввода-вывода|порта ввода-вывода]] и надо записать информацию сначала в один порт, потом в другой, это надо делать по одному байту: порядок прихода байтов на оборудование не гарантируется.
В процессорах [[x86]], если есть два последовательных [[порт ввода-вывода|порта ввода-вывода]] и требуется записать информацию сначала в один порт, затем в другой это следует делать по одному байту, поскольку порядок прихода байтов на оборудование не гарантируется.


Ещё один пример неопределенного поведения: курьёз с [[ANSI]]-[[директива компилятора|директивой]] «#pragma». Согласно спецификации языка компиляторам предоставлена полная свобода при обработке этой конструкции. До версии 1.17 компилятор [[GCC]] при нахождении в исходном коде этой директивы [http://blog.djmnet.org/2008/08/05/a-pragmatic-decision/ пытался запустить Emacs с игрой «Ханойские башни»].
Ещё один пример неопределенного поведения: курьёз с [[ANSI]]-[[директива компилятора|директивой]] «#pragma». Согласно спецификации языка, компиляторам предоставлена полная свобода при обработке этой конструкции. До версии 1.17 компилятор [[GCC]] при нахождении в исходном коде этой директивы пытался запустить Emacs с игрой «Ханойские башни».<ref>{{Cite web |url=http://blog.djmnet.org/2008/08/05/a-pragmatic-decision/ |title=A Pragmatic Decision {{!}} D-Mac's Stuff<!-- Заголовок добавлен ботом --> |access-date=2009-03-21 |archive-date=2009-06-01 |archive-url=https://web.archive.org/web/20090601052649/http://blog.djmnet.org/2008/08/05/a-pragmatic-decision/ |deadlink=no }}</ref>


В качестве еще одного примера неопределенного поведения можно привести код:
В качестве ещё одного примера неопределённого поведения можно привести код:


<source lang="c">
<syntaxhighlight lang="c">
int i = 5;
int i = 5;
i = ++i + ++i;
i = ++i + ++i;
</syntaxhighlight>
</source>


При его выполнении переменная i может принять значения 13 или 14 для C/C++, 13 для [[Java]], [[PHP]] и [[C Sharp|C#]], 12 при реализации на [[LISP]]. Неопределенность в языке C/C++ связана с тем, что согласно стандартам С и С++ побочные эффекты (то есть инкремент в данном случае) могут быть применены в любой удобный для компилятора момент между двумя [[Точка следования|точками следования]].
При его выполнении переменная <code>i</code> может принять значения 13 или 14 для C/C++, 13 для [[Java]], [[PHP]] и [[C Sharp|C#]], 12 при реализации на [[LISP]]. Неопределенность в языках C и C++ связана с тем, что, согласно стандартам C и C++, побочные эффекты (то есть инкремент в данном случае) могут быть применены в любой удобный для компилятора момент между двумя [[Точка следования|точками следования]].


== Достоинства ==
== Достоинства ==
Строка 33: Строка 60:
* Недопущение ситуаций неопределённого поведения остаётся за программистом.
* Недопущение ситуаций неопределённого поведения остаётся за программистом.


== Ссылки ==
== Примечания ==
{{примечания}}
* [http://www.unix.org.ua/unix2/glava_26.htm Мобильность на уровне исходных текстов]


== Ссылки ==
{{Compu-lang-stub}}
* [http://www.unix.org.ua/unix2/glava_26.htm Мобильность на уровне исходных текстов] {{Wayback|url=http://www.unix.org.ua/unix2/glava_26.htm |date=20061011172950 }}
* [http://www.viva64.com/ru/b/0306/ Разыменовывание нулевого указателя приводит к неопределённому поведению] {{Wayback|url=http://www.viva64.com/ru/b/0306/ |date=20150402115001 }}


[[Категория:Ошибки программирования]]
[[Категория:Ошибки программирования]]
[[Категория:Концепции языков программирования]]
[[Категория:Концепции языков программирования]]

[[en:Undefined behavior]]
[[pt:Comportamento indefinido]]
[[uk:Невизначена поведінка]]
[[zh:未定义行为]]

Текущая версия от 18:28, 31 января 2024

Неопределённое поведение (англ. 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++, побочные эффекты (то есть инкремент в данном случае) могут быть применены в любой удобный для компилятора момент между двумя точками следования.

Достоинства

[править | править код]
  • Определение некоторых операций как «неопределённых» приводит подобные языки (характеризующиеся зачастую отсутствием встроенной проверки на предел массива и т. д.) к упрощению спецификации и некоторому увеличению гибкости.
  • Ускоряется работа программ (так как не нужно проверять всевозможные «маргинальные» случаи).

Недостатки

[править | править код]
  • Не гарантирует полной совместимости различных реализаций языка.
  • Недопущение ситуаций неопределённого поведения остаётся за программистом.

Примечания

[править | править код]
  1. Программирование на языке C/C++. Самоучитель. — Dialektika, 2003-01-01. — 348 с. — ISBN 9785845904607.
  2. Павловская Татьяна Александровна. C/C++. Процедурное и объектно-ориентированное программирование. Учебник для вузов. Стандарт 3-го поколения. — "Издательский дом ""Питер""", 2014-07-30. — 496 с. — ISBN 9785496001090.
  3. nasal demons. Jargon File. Дата обращения: 10 сентября 2023. Архивировано 7 февраля 2013 года.
  4. A Pragmatic Decision | D-Mac's Stuff. Дата обращения: 21 марта 2009. Архивировано 1 июня 2009 года.