Ситуация, когда код на языке C++ синтаксически валиден, однако его поведение не определено в Стандарте, в русскоязычной литературе часто называют просто неопределённым поведением. В самом же Стандарте для таких ситуаций существуют целых 3 термина: undefined behavior, unspecified behavior и implementation-defined behavior. В этой коротенькой заметке мы будем разбираться, чем они отличаются.
Implementation-defined behavior
Этот термин применяется для описания ситуаций, когда C++ код полностью валиден, но его поведение зависит от реализации (например, компилятора или среды исполнения), и это поведение документировано. Например, размер в байтах указателя или типа int зависит от конкретной реализации или настроек компилятора, но это описано в документации, и на эту документацию можно полагаться.
Unspecified behavior
Термин означает, что поведение валидного C++ кода не определено Стандартом и зависит от реализации, к тому же не документировано (по крайней мере, официально). Пример: порядок вычисления значений аргументов функции определяется компилятором, но нигде нет описания, как именно. Стандарт говорит нам: это особенности поведения, которые не зафиксированы нигде, следовательно, на них нельзя полагаться. Поэтому и поведение вашего кода не должно зависеть от этих особенностей.
Undefined behavior
Это самый опасный вариант неопределённости. В Стандарте он служит для описания поведения, которое может привести к полностью непредсказуемым последствиям. Самые яркие примеры: обращение за границы массива или разыменование указателя на освобождённый объект. Хуже всего то, что программа совершенно не обязательно сразу же завершится или вообще выдаст какую-либо ошибку, тем не менее, на её поведение уже нельзя полагаться.
В заключение ещё раз напомню, что все вышеописанные термины относятся к синтаксически валидному коду, который будет успешно скомпилирован (впрочем, компиляторы зачастую выдают предупреждения для наиболее очевидных случаев undefined behavior). Код, невалидный с точки зрения Стандарта, называется ill-formed program.
Комментарии (27)
Cryptosoft
07.05.2019 23:20+1Автор не стал приводить примеры, так он решил.
Но вот классический пример Unspecified behavior для языка C.
int a,b,c;
a=b=0;
c = a+++b;
printf(«Value a=%d\n», a);
igorsemenov Автор
07.05.2019 23:38Автор решил не приводить примеры неопределённого поведения, т.к. они подробно разжёваны, в том числе и на этом сайте. Автор всего лишь попытался показать разницу между undefined и unspecified behavior, о чём честно сказал в первом абзаце.
P.S.: я кстати не понял, где там в примере Unspecified behavior? a == 1, c == 0. Разве нет?Cryptosoft
08.05.2019 00:06В зависимости от вида компилятора либо a==1, b==0, либо a==0, b==1.
jcmvbkbc
09.05.2019 00:52int a,b,c; a=b=0; c = a+++b; printf(«Value a=%d\n», a);
В зависимости от вида компилятора либо a==1, b==0, либо a==0, b==1.
Эта программа не содержит ничего из описанного в статье и в результате её выполненияa
всегда равно 1. Выражениеa+++b
трактуется компилятором соответствующим стандарту как(a++) + b
. В С99 это описано в пункте стандарта 6.4, в С++98 — в пункте 2.4:3.
NotThatEasy
08.05.2019 11:07Почитайте про точки следования в программе.
Undefined behavior – Это самый опасный вариант неопределённости. В Стандарте он служит для описания поведения, которое может привести к полностью непредсказуемым последствиям
Дело в том, что описания UB как раз отсутствуют в Стандарте.
Можно было привести определение, которую сразу же узнаёт любой, кто бывал на стековерфлоу или даже википедии – UB вызывается ситуациями, не описанными конкретно в спецификации языка.igorsemenov Автор
08.05.2019 11:09Я в курсе про точки следования (как они назывались до C++11, кстати), спасибо.
> Дело в том, что описания UB как раз отсутствуют в Стандарте.
Не согласен. Как раз именно в Стандарте все неопределённые ситуации и описаны в терминах UndB, UnspB и IDB. И все определения там есть, в самом начале.NotThatEasy
08.05.2019 11:54как они назывались до C++11, кстати
Fun fact, и вправду, благодарю за наводку.
Я стандарт максимум листал (слишком уж сухо), могу, конечно, ошибаться, транслировал когда-то вычитанное на стековерфлоу.
Судя по комменту, у Вас всё в порядке с сями; судя по статье, Вы умеете писать, не вызывая к себе неприязни.
Если Вас не затруднит обуздать слог и расписать полёт мысли, Хабр мог бы пополниться парой занятных плюсовых статей про тот же UB (Да, таких не одна, однако, нередко последующие добавляют что-то новое, либо же разный слог понятен разным группам людей).igorsemenov Автор
08.05.2019 11:59Спасибо за лестный отзыв!
Я стараюсь писать о чём-то новом (в том числе и для себя), а не переписывать одно и то же другими словами. Но если узнаю что-то действительно новое про UB — постараюсь написать и об этом.
Amomum
08.05.2019 02:14+1Вот пример попроще:
foo( bar(), buzz() );
— где не специфицирован порядок вызова bar и buzz. Гарантируется, что оба завершатся до вызова foo.Nick_Shl
08.05.2019 18:57Плохой пример. Это. Лучше:
int Div(int a, int b) {return (a/b);} int main() { int arr[2] = {0, 1}; int idx = 0; int c = Div(arr[idx++], arr[idx++]); printf(«Value c=%d\n», c); }
jcmvbkbc
09.05.2019 00:57int c = Div(arr[idx++], arr[idx++]);
В этой строке — неопределённое поведение, поскольку idx модифицируется дважды между точками следования. Выбор не между Div(0, 1) и Div(1, 0), а между запуском ядерных ракет и пробуждением кт-ху.Nick_Shl
09.05.2019 01:07Если вам не нравится именно то, что "idx модифицируется дважды", то можно написать и так:
В этом случае из-за неустановленного порядка вычисления аргументов функции результат может быть как 0/1, так и 0/0.int c = Div(arr[idx++], arr[idx]);
jcmvbkbc
09.05.2019 01:16int c = Div(arr[idx++], arr[idx]);
Здесь по-прежнему есть неопределённое поведение, поскольку выражение не удовлетворяет второй части того же самого требования стандартов C99 6.5:2/C++98 5:4:
Between the previous and next sequence point an object shall have its stored value modified at most once by the evaluation of an expression. Furthermore, the prior value shall be read only to determine the value to be stored.
vamireh
08.05.2019 12:00+1Здесь нет неопределённого поведения.
И путаете вы этот пример с чем-то типа такого
int i = 0; i = i++ + ++i;
Если же вы думаете, что три плюса слитно могут быть разобраны по-разному — как ++ и + в одном случае, и + и ++ — в другом, — то вы ошибаетесь. Парсинг стандартизирован, и должен быть так называемым «жадным». Т.е. всегда будет разбираться как ++ и +. Компилятор, который делает иначе — неправильный компилятор и делает неправильныймёдкод.
vamireh
08.05.2019 12:14Ситуация, когда код на языке C++ синтаксически валиден, однако его поведение не определено в Стандарте, в русскоязычной литературе часто называют просто неопределённым поведением.
Во-первых, код не просто синтаксически валиден, но и прошёл проверку типов и семантики.
Во-вторых, не знаю какую русскоязычную литературу читали вы, но в той, которую читал я, путаницы нет: неопределённым поведением называют именно undefined behavior.
Лучше (точнее, полнее) было бы просто перевести страницу на cppreference.jcmvbkbc
09.05.2019 01:00Лучше (точнее, полнее) было бы просто перевести страницу на cppreference
cppreference, конечно, популярный ресурс, но всё-таки это просто wiki в интернете. Лучше использовать стандарт.
multiprogramm
08.05.2019 13:09В заключение ещё раз напомню, что все вышеописанные термины относятся к синтаксически валидному коду, который будет успешно скомпилирован. Код, невалидный с точки зрения Стандарта, называется ill-formed program.
Вот хотел ещё уточнить, может быть кто-нибудь из присутствующих подскажет. Я так понимаю, что если код не компилируется, то это значит, что он является ill-formed. Однако обратное неверно: код может скомпилироваться, но всё равно являться ill-formed, например, если в коде нарушено ODR. Так ли это? Нет ли ещё каких-нибудь вариантов компилирующегося кода, который при этом будет ill-formed?
И ещё такой вопрос по поводу успешности компиляции: а не может ли UB всё-таки вызвать ошибку компиляции (просто как возможность, не обязательно даже стабильное повторение)? Оно же всё-таки U. Может при развёртывании каких-нибудь шаблонов или макросов, например?
Хочется для себя по полочкам разложить, как эти все круги диаграммы Венна пересекаются, и что есть их пересечения: Implementation-defined behavior, Unspecified behavior, Undefined behavior, ill-formed, некомпилирующийся код.igorsemenov Автор
08.05.2019 13:12Многие виды UB вызывают ворнинги в GCC и Clang.
Кстати, сейчас для определения UB во многих случаях можно писать constexpr-функции. По новому Стандарту UB недопустим в constexpr-функциях, поэтому код не скомпилируется.
KanuTaH
08.05.2019 16:08alenacpp.blogspot.com/2005/08/unspecified-behavior-undefined.html
Это про первые три подробно. Ill-formed по стандарту — это отрицание термина «well-formed» («program that is not well formed»). Well-formed — это «program constructed according to the syntax rules, diagnosable semantic rules, and the One Definition Rule». То есть это в основном то, что компилируется, но бывают исключения типа «ill-formed, no diagnostic required», тогда это по сути ничем не отличается от undefined behaviour, если компилятор все-таки не реализует эту диагностику (но он может, хоть и не обязан — просто на данный случай нет такого требования).
vilgeforce
Слишком много примеров!
RussDragon
Я бы на цикл статей разбил материал.
Vest
Согласен. Много информации, а всего лишь вторник.
vilgeforce
Тема-то богатая, но автор решил иначе