Неопределённое поведение в C++


    Ситуация, когда код на языке C++ синтаксически валиден, однако его поведение не определено в Стандарте, в русскоязычной литературе часто называют просто неопределённым поведением. В самом же Стандарте для таких ситуаций существуют целых 3 термина: undefined behavior, unspecified behavior и implementation-defined behavior. В этой коротенькой заметке мы будем разбираться, чем они отличаются.


    Implementation-defined behavior


    Этот термин применяется для описания ситуаций, когда C++ код полностью валиден, но его поведение зависит от реализации (например, компилятора или среды исполнения), и это поведение документировано. Например, размер в байтах указателя или типа int зависит от конкретной реализации или настроек компилятора, но это описано в документации, и на эту документацию можно полагаться.


    Unspecified behavior


    Термин означает, что поведение валидного C++ кода не определено Стандартом и зависит от реализации, к тому же не документировано (по крайней мере, официально). Пример: порядок вычисления значений аргументов функции определяется компилятором, но нигде нет описания, как именно. Стандарт говорит нам: это особенности поведения, которые не зафиксированы нигде, следовательно, на них нельзя полагаться. Поэтому и поведение вашего кода не должно зависеть от этих особенностей.


    Undefined behavior


    Это самый опасный вариант неопределённости. В Стандарте он служит для описания поведения, которое может привести к полностью непредсказуемым последствиям. Самые яркие примеры: обращение за границы массива или разыменование указателя на освобождённый объект. Хуже всего то, что программа совершенно не обязательно сразу же завершится или вообще выдаст какую-либо ошибку, тем не менее, на её поведение уже нельзя полагаться.


    В заключение ещё раз напомню, что все вышеописанные термины относятся к синтаксически валидному коду, который будет успешно скомпилирован (впрочем, компиляторы зачастую выдают предупреждения для наиболее очевидных случаев undefined behavior). Код, невалидный с точки зрения Стандарта, называется ill-formed program.

    Реклама
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее

    Комментарии 30

      +25
      Слишком много примеров!
        +22
        Я бы на цикл статей разбил материал.
          +15

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

            0
            Тема-то богатая, но автор решил иначе
          0
          Автор не стал приводить примеры, так он решил.
          Но вот классический пример Unspecified behavior для языка C.

          int a,b,c;
          a=b=0;
          c = a+++b;
          printf(«Value a=%d\n», a);

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

            P.S.: я кстати не понял, где там в примере Unspecified behavior? a == 1, c == 0. Разве нет?
              0
              В зависимости от вида компилятора либо a==1, b==0, либо a==0, b==1.
                0
                Любопытно. А в каком компиляторе a==0, b==1?
                  0
                  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.
                    0
                    Да, верно. Но всё равно лучше так программы не писать, поскольку можно случайно сделать непреднамеренную ошибку. Правда, это уже по другой теме.
                  0
                  Почитайте про точки следования в программе.
                  Undefined behavior – Это самый опасный вариант неопределённости. В Стандарте он служит для описания поведения, которое может привести к полностью непредсказуемым последствиям

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

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

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

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

                    0
                    Плохой пример. Это. Лучше:
                    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);
                    }
                      0

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

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

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

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

                        В этой строке — неопределённое поведение, поскольку idx модифицируется дважды между точками следования. Выбор не между Div(0, 1) и Div(1, 0), а между запуском ядерных ракет и пробуждением кт-ху.
                          0
                          Если вам не нравится именно то, что "idx модифицируется дважды", то можно написать и так:
                          int c = Div(arr[idx++], arr[idx]);
                          В этом случае из-за неустановленного порядка вычисления аргументов функции результат может быть как 0/1, так и 0/0.
                            0
                            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.
                              0
                              Да, в C++ до 17 года так же. Но в современном оба примера будут давать лишь неспецифицированный порядок вычисления, и лишь если вторым аргументом окажется 0, то тогда при делении получим неопределённое поведение.
                              7.6.1.2/8
                      +1
                      Здесь нет неопределённого поведения.
                      И путаете вы этот пример с чем-то типа такого
                      int i = 0;
                      i = i++ + ++i;


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

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

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

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

                          Для чего? Для написания статьи а-ля «в стандарте есть пункты x, y и z — читайте их, чтобы всё понять»?
                          Дело в том, что «просто wiki в интернете», книги, статьи и т.п. существуют для того, чтобы 1) учиться и 2) не лазить по перекрёстным ссылкам суконного языка стандарта. Да, конечно, остаётся вопрос доверия, потому первоисточник является непреложной истиной. Но это справочник, а не учебное пособие.
                          Потому я и говорю, что если было желание написать такую статью, то перевод весьма качественной, на мой взгляд, wiki был бы полезнее. Да, можно было бы озаботиться и проставлением ссылок на стандарт.
                        0
                        В заключение ещё раз напомню, что все вышеописанные термины относятся к синтаксически валидному коду, который будет успешно скомпилирован. Код, невалидный с точки зрения Стандарта, называется ill-formed program.

                        Вот хотел ещё уточнить, может быть кто-нибудь из присутствующих подскажет. Я так понимаю, что если код не компилируется, то это значит, что он является ill-formed. Однако обратное неверно: код может скомпилироваться, но всё равно являться ill-formed, например, если в коде нарушено ODR. Так ли это? Нет ли ещё каких-нибудь вариантов компилирующегося кода, который при этом будет ill-formed?
                        И ещё такой вопрос по поводу успешности компиляции: а не может ли UB всё-таки вызвать ошибку компиляции (просто как возможность, не обязательно даже стабильное повторение)? Оно же всё-таки U. Может при развёртывании каких-нибудь шаблонов или макросов, например?
                        Хочется для себя по полочкам разложить, как эти все круги диаграммы Венна пересекаются, и что есть их пересечения: Implementation-defined behavior, Unspecified behavior, Undefined behavior, ill-formed, некомпилирующийся код.
                          0
                          Многие виды UB вызывают ворнинги в GCC и Clang.
                          Кстати, сейчас для определения UB во многих случаях можно писать constexpr-функции. По новому Стандарту UB недопустим в constexpr-функциях, поэтому код не скомпилируется.
                            0
                            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, если компилятор все-таки не реализует эту диагностику (но он может, хоть и не обязан — просто на данный случай нет такого требования).

                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                          Самое читаемое