Ситуация, когда код на языке 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)


  1. vilgeforce
    07.05.2019 19:16
    +16

    Слишком много примеров!


    1. RussDragon
      07.05.2019 20:03
      +11

      Я бы на цикл статей разбил материал.


      1. Vest
        07.05.2019 20:13
        +8

        Согласен. Много информации, а всего лишь вторник.


      1. vilgeforce
        07.05.2019 22:06

        Тема-то богатая, но автор решил иначе


  1. 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);


    1. igorsemenov Автор
      07.05.2019 23:38

      Автор решил не приводить примеры неопределённого поведения, т.к. они подробно разжёваны, в том числе и на этом сайте. Автор всего лишь попытался показать разницу между undefined и unspecified behavior, о чём честно сказал в первом абзаце.

      P.S.: я кстати не понял, где там в примере Unspecified behavior? a == 1, c == 0. Разве нет?


      1. Cryptosoft
        08.05.2019 00:06

        В зависимости от вида компилятора либо a==1, b==0, либо a==0, b==1.


        1. igorsemenov Автор
          08.05.2019 00:12

          Любопытно. А в каком компиляторе a==0, b==1?


        1. jcmvbkbc
          09.05.2019 00:52

          int 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.


      1. NotThatEasy
        08.05.2019 11:07

        Почитайте про точки следования в программе.

        Undefined behavior – Это самый опасный вариант неопределённости. В Стандарте он служит для описания поведения, которое может привести к полностью непредсказуемым последствиям

        Дело в том, что описания UB как раз отсутствуют в Стандарте.
        Можно было привести определение, которую сразу же узнаёт любой, кто бывал на стековерфлоу или даже википедии – UB вызывается ситуациями, не описанными конкретно в спецификации языка.


        1. igorsemenov Автор
          08.05.2019 11:09

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

          > Дело в том, что описания UB как раз отсутствуют в Стандарте.
          Не согласен. Как раз именно в Стандарте все неопределённые ситуации и описаны в терминах UndB, UnspB и IDB. И все определения там есть, в самом начале.


          1. NotThatEasy
            08.05.2019 11:54

            как они назывались до C++11, кстати

            Fun fact, и вправду, благодарю за наводку.
            Я стандарт максимум листал (слишком уж сухо), могу, конечно, ошибаться, транслировал когда-то вычитанное на стековерфлоу.
            Судя по комменту, у Вас всё в порядке с сями; судя по статье, Вы умеете писать, не вызывая к себе неприязни.
            Если Вас не затруднит обуздать слог и расписать полёт мысли, Хабр мог бы пополниться парой занятных плюсовых статей про тот же UB (Да, таких не одна, однако, нередко последующие добавляют что-то новое, либо же разный слог понятен разным группам людей).


            1. igorsemenov Автор
              08.05.2019 11:59

              Спасибо за лестный отзыв!
              Я стараюсь писать о чём-то новом (в том числе и для себя), а не переписывать одно и то же другими словами. Но если узнаю что-то действительно новое про UB — постараюсь написать и об этом.


    1. Amomum
      08.05.2019 02:14
      +1

      Вот пример попроще:
      foo( bar(), buzz() ); — где не специфицирован порядок вызова bar и buzz. Гарантируется, что оба завершатся до вызова foo.


      1. 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);
        }


        1. Amomum
          08.05.2019 21:05

          А чем мой плох?


          1. Nick_Shl
            08.05.2019 22:15

            Какая разница какая функция вызовется первой bar() или buzz()? Без контекста что внутри это не так наглядно.
            А вот когда вопрос стоит в том, будет делится 0 на 1 или 1 на 0 — это совсем другое дело.


            1. Amomum
              08.05.2019 22:15
              +1

              Резонно, спасибо.


        1. jcmvbkbc
          09.05.2019 00:57

          int c = Div(arr[idx++], arr[idx++]);

          В этой строке — неопределённое поведение, поскольку idx модифицируется дважды между точками следования. Выбор не между Div(0, 1) и Div(1, 0), а между запуском ядерных ракет и пробуждением кт-ху.


          1. Nick_Shl
            09.05.2019 01:07

            Если вам не нравится именно то, что "idx модифицируется дважды", то можно написать и так:

            int c = Div(arr[idx++], arr[idx]);
            В этом случае из-за неустановленного порядка вычисления аргументов функции результат может быть как 0/1, так и 0/0.


            1. jcmvbkbc
              09.05.2019 01:16

              int 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.


    1. vamireh
      08.05.2019 12:00
      +1

      Здесь нет неопределённого поведения.
      И путаете вы этот пример с чем-то типа такого

      int i = 0;
      i = i++ + ++i;


      Если же вы думаете, что три плюса слитно могут быть разобраны по-разному — как ++ и + в одном случае, и + и ++ — в другом, — то вы ошибаетесь. Парсинг стандартизирован, и должен быть так называемым «жадным». Т.е. всегда будет разбираться как ++ и +. Компилятор, который делает иначе — неправильный компилятор и делает неправильный мёд код.


  1. vamireh
    08.05.2019 12:14

    Ситуация, когда код на языке C++ синтаксически валиден, однако его поведение не определено в Стандарте, в русскоязычной литературе часто называют просто неопределённым поведением.

    Во-первых, код не просто синтаксически валиден, но и прошёл проверку типов и семантики.
    Во-вторых, не знаю какую русскоязычную литературу читали вы, но в той, которую читал я, путаницы нет: неопределённым поведением называют именно undefined behavior.

    Лучше (точнее, полнее) было бы просто перевести страницу на cppreference.


    1. jcmvbkbc
      09.05.2019 01:00

      Лучше (точнее, полнее) было бы просто перевести страницу на cppreference

      cppreference, конечно, популярный ресурс, но всё-таки это просто wiki в интернете. Лучше использовать стандарт.


  1. 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, некомпилирующийся код.


    1. igorsemenov Автор
      08.05.2019 13:12

      Многие виды UB вызывают ворнинги в GCC и Clang.
      Кстати, сейчас для определения UB во многих случаях можно писать constexpr-функции. По новому Стандарту UB недопустим в constexpr-функциях, поэтому код не скомпилируется.


    1. KanuTaH
      08.05.2019 16:08

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