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

Достаточно сложной темой для программистов на С++ является undefined behavior. Даже опытные разработчики зачастую не могут четко сформулировать причины его возникновения. Статья призвана внести чуть больше ясности в этот вопрос.

Статья является ПЕРЕВОДОМ нескольких статей и выдержек из Стандарта по данной теме.

Что такое «точки следования»?

Стандарте сказано:
Точки следования (sequence points)– такие точки в процессе выполнения программы, в которых все побочные эффекты уже выполненного кода закончили свое действие, а побочные эффекты кода, подлежащего исполнению, еще не начали действовать. (§1.9/7)


Побочные эффекты? А что такое «побочные эффекты»?

Побочный эффект (side effect) (согласно Стандарту) – результат доступа к volatile объекту, изменения объекта, вызова функции из библиотеки I/O или же вызова функции, включающей в себя какие-то из этих действий. Побочный эффект является изменением состояния среды выполнения.

Вычисление некоторого выражения дает на выходе какой-то результат. Если же в дополнение к результату вычисление выражения вызывает изменения в среде выполнения, то говорят, что данное выражение имеет побочные эффекты.

Например:

int x = y++;   //  «y» тоже int


В дополнение к операции инициализации переменной «x» значение переменной «y» изменилось из-за побочного эффекта оператора ++.
Что ж, с этим понятно. Далее к точкам следования. Альтернативное определение понятия «точка следования» дано Стивом Саммитом (автор книг «Язык C в вопросах и ответах», блога «comp.lang.c»):
Точка следования – момент времени, когда «пыль улеглась», и все встреченные побочные эффекты гарантированно завершены и остались позади.

Какие точки следования описаны в Стандарте C++?

В стандарте описаны следующие точки следования:

  • в конце вычисления полного выражения (§1.9/16). Под «полным выражением» (full-expression) подразумевается выражение, не являющееся подвыражением (subexpression) — частью другого выражения (прим: вычисление полного выражения может включать в себя вычисление подвыражения, лексически не являющегося его частью. Например, подвыражения, участвующие в вычислении аргумента по умолчанию, считаются частью выражения, которое вызвало функцию, а не выражения, определяющего этот аргумент).

    Например:

    int a = 5; // «;» - точка следования в данном контексте


  • в вычислении следующих выражений, а именно после вычисления первого операнда:

    1. a && b (§5.14)
    2. a || b (§5.15)
    3. a? b: c (§5.16)
    4. a, b (§5.18)

    Вычисление этих выражений идет слева направо. После вычисления левого подвыражения все побочные эффекты этого вычисления прекращают действие. Если после вычисления левого подвыражения значение полного выражения известно, то правая часть не вычисляется. В последнем случае имеется в виду оператор запятая. В функции func(a, a++) запятая – не оператор, а просто разделитель между аргументами.

  • при вызове функции (неважно, является функция встроенной или нет) после вычисления всех ее аргументов (если таковые имеются) и перед выполнением каких-либо инструкций в ее теле.


Что такое «неопределенное поведение»?

Стандарт дает определение словосочетанию «неопределенное поведение» в §1.3.12:
Неопределенное поведение (undefined behavior)– поведение, которое может возникать в результате использования ошибочных программных конструкций или некорректных данных, на которые Международный Стандарт не налагает никаких требований. Неопределенное поведение также может возникать в ситуациях, не описанных в Стандарте явно.

Иными словами, неопределенное поведение означает что угодно, что может произойти, начиная от козявки, выпавшей из носа, заканчивая беременностью вашей девушки.

Какая связь между неопределенным поведением и точками следования?

Перед тем, как узнать ответ на этот вопрос, вы должны понять, в чем различия между неопределенным поведением, неуточняемым поведением и поведением, определяемым реализацией.
Неуточняемое поведение (unspecified behavior) (согласно Стандарту) – поведение, для которого Стандарт предлагает два или более возможных вариантов и не налагает четких требований на то, какой из них должен быть выбран в определенной ситуации.

Неуточняемое поведение возникает в результате вычисления таких подвыражений, как:
  • аргументы в вызове функции
  • операнды операторов (напр. +, -, =, *, /), за исключением:
    1. операторов бинарной логики (&& и ||)
    2. оператора условия (?:)
    3. оператора запятой.

    (прим.: за исключением именно тех операторов, которые содержат точку следования)

Поведение, определяемое реализацией (implementation-defined behavior) (согласно Стандарту) – поведение правильно сформированной программной конструкции с правильными данными, которое зависит от реализации (должно быть документировано для каждой реализации).

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

Например:

int x = 5, y = 6;
int z = x++ + y++; // не уточнено, будет вычислен первым x++ или y++ 


Еще один пример:


  int Hello()
  {
       return printf("Hello"); 
  }

  int World()
  {
       return printf("World !");
  }

  int main()
  {

      int a = Hello() + World(); /**может вывести «Hello World!» или «World! Hello»
                      ^
                      | 
                Функции могут быть вызваны в любом порядке **/
      return 0;
  } 


В §5/4 Стандарт говорит:
Между двумя точками следования скалярный объект должен менять хранимое значение при вычислении выражения не более одного раза.

Что это значит?

Говоря чуть проще, переменную между двумя точками следования нельзя менять больше одного раза. В выражении следующая точка следования обычно располагается на заключающей точке с запятой, а предыдущая – в конце предшествующего оператора. Выражение так же может содержать промежуточные точки следования.
Исходя из вышесказанного, следующие выражения создают неопределенное поведение:


i++ * ++i; // 
i = ++i;   // 
++i = 2;   //    i изменено более 1 раза
i = ++i +1 // 
i = (i,++i,++i); // нет точки следования между правым `++i` и присвоением `i` (`i` изменяется более 1 раза между 2мя точками следования)  

Но в то же время:


i = (i, ++i, 1) + 1; // определено
i = (++i,i++,i)     //  определено
int j = i;
j = (++i, i++, j*i); // определено 


Кроме того (по Стандарту) – старое значение выражения (до вычисления) должно быть доступно только для определения хранимого значения.
Это значит, что некорректными являются те выражения, в которых доступ к значению может предшествовать его модификации.

Например:


std::printf("%d %d", i,++i); // неизвестно, что произойдет раньше – вычисление (++i) или доступ к нему.


Еще один пример:


a[i] = i++ ;   // либо a[++i] = i , либо  a[i++] = ++i  и т.д. 


Я слышал, что в C++0x нет никаких Точек Следования, это правда?


Да, это правда.
Понятие «точка следования» было заменено комитетом ISO C++ на уточненное и дополненное понятие Отношения Следования [ДО\ПОСЛЕ].

Что такое Отношение Следования[ДО]?
Следование ДО (Sequenced Before) это отношение, которое:
  • ассиметрично
  • транзитивно
  • возникает между парами вычислений и формирующее из них частично упорядоченное множество (partially ordered set)


Формально, это означает, что при двух данных выражениях А и B, если А [следует ДО] B, то выполнение А должно предшествовать выполнению В. Если же А не [следует ДО] В, тогда выполнение А и В является неупорядоченным (unsequenced) (выполнение неупорядоченных вычислений может пересекаться).
Вычисление A и В являются неопределенно упорядоченным (indeterminantly sequenced), когда либо А [следует ДО] В, либо В [следует ДО] А, но что именно – не уточнено. Неопределенно упорядоченные вычисления не могут пересекаться, но любое из них может выполнятся первым.

Что означает слово «вычисление» в контексте C++0x ?

В С++0x вычисление (evaluation) выражения (или подвыражения) в общем случае включает в себя:
  • подсчет (computation) значения (включая определение положения объекта в памяти для вычисления значения gvalue-выражения и получение значения по ссылке для вычисления prvalue-выражения)
  • инициирование побочных эффектов


Стандарт говорит нам (§1.9/14):
Каждый подсчет значения и побочный эффект, связанные с полным выражением, [следуют ДО] подсчета значения и побочного эффекта, связанных со следующим полным выражением, которое будет вычислено.

Тривиальный пример:

int x;  x = 10;  ++x; 


В данном примере подсчет значения и побочный эффект, связанный с выражением (++x), [следует ПОСЛЕ] подсчета значения и побочного эффекта (x=10).

Ведь между вещами, описанными выше, и неопределенным поведением должна быть какая-то связь, да?


Конечно, связь есть.

В §1.9/15 упоминается, что:
Вычисление операндов конкретного оператора или подвыражений конкретного выражения неупорядоченно, помимо случаев, которые были описаны ранее.

Примечание: неупорядоченные и неопределенно упорядоченные подвыражения полного выражения, которое вычисляется более одного раза в процессе выполнения программы, не обязательно вычисляются каждый раз в одном и том же порядке.

Например:


int main()
{
     int num = 19 ;
     num = (num << 3) + (num >> 3) ;
} 


1) Вычисление операндов оператора «+» неупорядоченно.
2) Вычисление операндов операторов «<<» и «>>» неупорядоченно.

§1.9/15 подсчет значения операндов конкретного оператора [следует ДО] подсчета значения результата работы оператора.

Это означает, что в выражении x + y подсчет значений «х» и «у» [следует ДО] подсчета x+y.

Теперь к более важному:

§1.9/15 Если возникновение побочного эффекта скалярного объекта неупорядоченно по отношению к одному из следующий событий:
  • возникновению другого побочного эффекта этого же объекта
  • подсчету значения с использованием значения этого объект

то поведение программы будет НЕОПРЕДЕЛЕННЫМ.


Пример:

f(i  =  -1,  i  =  -1);


Поясним это выражение. На первый взгляд, неупорядоченность вычисления аргументов функции не повлечет за собой неоднозначности. Однако нет точной вероятности, что компилятор, оптимизируя подобное выражение, не создаст набор инструкций, аналогичный по действию (по его мнению), который даст сбой при операции над одной и той же памятью.

Предположим, что компилятор решил, что оптимальнее всего присвоить "-1" будет обнулив переменную и сделав ее декремент.
Инструкции могут сформироваться так (команды условны):
clear i
decr  i
clear  i
decr  i


А могут так:
clear i
clear  i
decr  i
decr  i


после чего в i будет хранится значение -2.

При вызове функции каждый подсчет значения и побочный эффект, связанный с выражением аргумента этой функции, или с выражением, вызывающим функцию, [следует ДО] выполнения любого выражения или оператора в теле вызываемой функции.
Подсчет значения и побочные эффекты, связанные с разными аргументами, неупорядоченны.

Поток выполнения программы


Оперируя терминами, расшифрованными ранее, поток выполнения программы можно представить графически. В следующих далее диаграммах обозначим вычисление выражения (или подвыражения) как E(x), точку следования — %, побочный эффект «k» для объекта «e» обозначим S(k,e). Если для вычисления необходимо считать значение из именованного объекта (пусть «x» — имя), вычисление будем обозначать V(x), в остальных случаях – так же, как договаривались ранее, E(x). Побочные эффекты запишем справа и слева от выражений. Граница между двумя выражениями обозначает, что верхнее выражение вычисляется до нижнего выражения (зачастую потому что нижнее выражение зависит от prvalue или lvalue верхнего выражения).
Для двух выражений i++; i++; диаграмма будет иметь вид:

E(i++) -> { S(increment, i) }
   |
   %
   |
E(i++) -> { S(increment, i) }
   |
   % 


Как видите, в данном случае мы имеем две точки следования, одна из которых разделяет два изменения «i».
Вызовы функций также представляют интерес, несмотря на то, что диаграмму для них мы опустим:


int c = 0;
int d = 0;
void f(int a, int b) { assert((a == c - 1) && (b == d - 1)); }
int main() { f(c++, d++); } 


Этот код корректный, потому что к тому времени, как начнет выполняться тело функции f, все побочные эффекты, порожденные вычислением аргументов, гарантированно закончатся: «с» и «d» будут увеличены на 1.
Теперь рассмотрим выражение i++ * j++;
{ S(increment, i) } <- E(i++)      E(j++) -> { S(increment, j) }
                           \       /
                            +--+--+
                               |
                         E(i++ * j++)
                               |
                               % 


Откуда же появилось две ветки? Напомним, что точки следования завершают вычисления, проводившиеся ДО их наступления. Все подвыражения умножения вычисляются до самого умножения, больше в этом выражении нет точки следования, следовательно, нам нужно принять во внимание теоретическую «параллельность» вычисления операндов, чтобы предположить, где может произойти конкурентное изменение одного и того же объекта. Говоря более формально, эти две ветви неупорядочены. Точки следования – это отношение, которое упорядочивает некоторые вычисления и не упорядочивает другие. Т.о. точки следования, как и говорилось выше, являются частичным упорядочиванием (partial order).

Конфликтующие побочные эффекты.

Чтобы обеспечить компилятору свободу в генерации и оптимизации машинного кода, в случаях, подобных рассмотренному выше умножению, не устанавливается порядок вычисления подвыражений и не разделяются побочные эффекты, порожденные ими (за исключением описанных ранее случаев).
Это может вести к конфликтам, поэтому Стандарт называет неопределенным поведение программы, если она пытается модифицировать один и тот же объект без участия точек следования. Это относится к скалярным объектам, потому что остальные объекты являются либо неизменяемыми (array) или попросту не подпадают под это правило (class objects). Неопределенное поведение также возникает, если в выражении присутствуют обращение к предыдущему значению объекта и его модификация, как например в i * i++
// Ведет к неопределенному поведению!
// Не факт, что из левого 'i' будет считано «новое» значение:

    V(i)        E(i++) -> { S(increment, i) })
      \         /
       +---+---+
           |
       E(i * i++)
           |
           %

В качестве исключения позволено считывать значение объекта, если оно необходимо для подсчета нового значения. Пример контекста: i = i+1
                V(i)        E(1)
                   \         /
                    +---+---+
                        |
  E(i)              E(i + 1)
     \                 /
      +-------+-------+
              |
        E(i = i + 1) -> { S(assign, i) }
              |
              %


Здесь мы видим обращение к «i» в правой части; после вычисления обеих частей совершается присваивание. Т.о. побочный эффект и обращение к «i» происходят, не пересекая точку следования, но обращались к «i» мы только для определения хранимого значения, поэтому разногласий не будет.
Иногда, значение считывается после модификации. Для случая
a = (b = 0);
справедливо, что происходит запись в «b», а потом чтение из «b» без пересечения точки следования. Тем не менее, это нормально, потому что считывается уже новое значение «b», а обращения к старому не происходит. В этом случае побочные эффекты присвоения «b» закончат свое действие не только до следующей точки следования, но и перед чтением значения «b», требуемого для присвоения «а». Стандарт явно говорит: «результатом операции присваивания является значение, хранимое в левом операнде, после того, как присваивание выполнено (результат — lvalue)». Почему не используется понятие точки следования? Потому что это понятие содержит ненужное в данной ситуации требование, чтобы все побочные эффекты левого и правого операнда были завершены, вместо того чтобы рассматривать только побочные эффекты присвоения, возвращающего lvalue, с помощью которого происходит считывание.

Источники:
Поделиться публикацией
Ой, у вас баннер убежал!

Ну, и что?
Реклама
Комментарии 40
  • +2
    Критика формулировок, замечания по содержанию и оформлению категорически приветствуются.
    • –2
      deleted
      • 0
        Может стОит прочитать статью?
      • –11
        Да пожалуйста. Понятие «точка следования» удалено из С++ начиная со стандарта С++11. Суть языка не изменилась, но вот термина такого больше нет. Пруф: en.wikipedia.org/wiki/Sequence_point
        • +8
          Я извиняюсь, но вы статью вообще читали? Цитата из середины:

          Я слышал, что в C++0x нет никаких Точек Следования, это правда?

          Да, это правда.
          Понятие «точка следования» было заменено комитетом ISO C++ на уточненное и дополненное понятие Отношения Следования [ДО\ПОСЛЕ].


          Или это тоже некорректное объяснение (сам не в курсе)?
          • +2
            Если быть точным, то было заменено на понятия отношений «sequenced before» / unsequenced (могут перекрываться).
            Немного смущает название статьи Undefined Behaviour при рассмотрении лишь одного из его аспектов (ведь UB встречается не только в вычислении выражений с побочными эффектами).
            • 0
              Да, про это там тоже дальше есть.
              • +1
                И про абсолютно другие виды UB тоже есть? :-)
            • +4
              В том-то и дело, что актуальную информацию убрали в середину статьи. Я тоже споткнулся на 4-м абзаце, но поборол желание написать комментарий и продолжил читать дальше.
              • +5
                Добавьте тэг «никто не читает статью полностью» :)
            • –5
              Не критика, но я бы не стал переводить выдержки из стандарта. По-моему, ухудшает понимание.
              • +2
                Статья про формализмы. Цитаты из стандарта тут как нельзя кстати.
                • 0
                  Я бы привел их в оригинале. Вы против? :-)
                  • +1
                    Ну даны ведь ссылки на пункты стандарта. Кто хочет прочитать в оригинале — легко может это сделать. По-моему, подход правильный.
              • 0
                Из текста следует что выражение «f(i = -1, i = -1);» есть неопределённое поведение. Но ведь это не так. Опечатка в выражении, или смысловая ошибка? Или оно всё же считается формально неопределённым, хотя и имеет строго определённый результат?
                • 0
                  Формально считается неопределенным (ISO C++ 1.9.15).
                  • 0
                    Это, ктсати, какая-то жесть. Можно рассуждать так: вычисление операндов функции любое. Если существуют два таких порядка вычисления, которые приводят к различным результатам, то это неопределённое поведение. Если не существует — то нет. А вот в этом примере такая логика не работает что ли?
                    • +3
                      Работает такая логика, что в данном коде совершается более одной модификации состояния i в ходе вычисления полного выражения. Компилятор не проводит анализа, позволяющего увидеть, что присваивается одно и то же значение.
                      • +1
                        Что значит «не проводит анализа»? Удаление общих подвыражений — стандартная оптимизация и все компиляторы её содержат.

                        Ну тут другая логика срабатывает первой:
                        1. У нас тут кусочек, который потенциально может вызвать неопределённое поведение.
                        2. Но у нас при всё притом программа на C/C++ (если кто-то компилирует с помощью компилятора что-то другое, то он ССЗБ).
                        2a. А мы знаем, что никакая программа на C/C++ в принципе никогда не может вызвать неопределённое поведение.
                        3. А это, в свою очередь, значит, что этот код никогда не исполняется.
                        4. Но так это же круто! Мы можем врубить на полную катушку, скажем удаление мёртвого кода и получить бааальшую экономию на этом.

                        Очень часто люди исходят из странной логики: «да, у меня тут неопределённое поведение, но, чёрт побери, что может случиться плохого?». А компилятор — он доверчив, он каждый такой кусок воспринимает как клятвенное обещание программиста. Типа: «мамой клянусь (ну или там — руками, ногами, зубами), что вот этот вот код исполняться не будет». Как вы этого собираетесь добиваться — это ваше дело, но если уж вы написали "if (this == NULL) { ... }", то тем самым пообещали компилятору, что то, что в фигурных скобках никогда исполнено не будет. Компилятор и саму проверку и весь этот код с превеликим удовольствием из программы удалит: она от этого только меньше и быстрее станет.
                        • 0
                          Вы сейчас объяснили дизайнерские решения, скрывающиеся за самим понятием undefined behaviour. Это как в случае с многопоточностью, программист обязуется писать data-race free код, а язык в ответ гарантирует детерменированное поведение программы.
                          Я же объяснил, почему здесь возникает UB (потому что происходит более одной модификации состояния одной и той же переменной). Мне не стоило писать по поводу проведения анализа, я не хотел отрицать dead code, common subexpression и прочие оптимизации :-)
                          • 0
                            Очень много вотзефаков у начинающих программистов возникает по поводу i++ + i++ и f(i=1,i=1).
                            Казалось бы, всё доступно компилятору, можно было бы и определиться (прокачать UB до implementation-defined, хотя бы).

                            А вы подумайте вот о каком коде, например:
                            void f(int, int);
                            void foo( int& x, int& y)
                            {
                              int z = x++ + y++;
                              f(x=1, y=1);
                            }
                            

                            • 0
                              Кстати говоря, просто не надо так писать. Подобный код это большие проблемы при автомержах, рефакторингах, и т.д, да и вообще читается так себе. Ведь достаточно просто написать красиво, что бы код был человекопонятным:
                              void f(int, int);
                              void foo( int& x, int& y)
                              {
                                int z = x+1 + y+1;
                                x = 1; y = 1;
                                f(x, y);
                              }
                              

                              Производительность при этом не только не пострадает, но легко может и возрасти, ибо для компилятора такой код понятнее, поэтому z компилятор может получить совсем красивым и простым путём (здесь имеется ввиду уже ассемблер):
                              int z = 2;
                              z += x;
                              z += y;
                              

                              В общем мораль неопределённого поведения проста — не усложняйте никому жизнь (себе, коллегам, компилятору).
                              А то и ещё более упростить:
                              void f(int, int);
                              void foo( int& x, int& y)
                              {
                                int z = x+1 + y+1;
                                x = 1; y = 1;
                                f(1, 1);
                              }
                              

                              и вуаля, оказывается очевидным любому, что f у нас с константами работает (компилятор обычно сам догадывается о таком при оптимизациях, а вот человеку читать гораздо легче).
                              • 0
                                Понятно, что «не надо так писать».
                                Я подчёркиваю, что UB в коде, который «так написан» возникает не по прихоти авторов стандарта.
                                • 0
                                  Агу, я Вас понял, просто дополнил, показав тем, кто будет потом читать, что такой код вызывает UB, в т.ч. и в головах у разработчиков, его читающих.

                                  p.s: агу, ошибся я, надо было в общую ветку написать.
                    • 0
                      видимо, имелось в виду, что в обоих случаях значение i будет -1. Поправил.
                      • 0
                        В оригинальной статье именно дважды «i = -1». Непонятно, опечатка или нет.
                        • +1
                          Я привёл ссылку на стандарт (1.9.15), там именно двойное i = -1.
                          • 0
                            Под оригинальной статьёй я имел ввиду ответ на stackoverflow. Точно, в стандарте именно такой пример, две «i = -1». Да, круто.
                        • 0
                          Исправляйте теперь назад :)
                        • +3
                          Наверняка так и задумано, и строго определенного результата не имеет.
                          stackoverflow.com/a/21671069
                          • 0
                            О, отличная ссылка, спасибо.
                            • 0
                              А вот эта ссылка даёт радикально лучшее понимание, чем стандарт (стандарт не даёт такого, потому что по ссылке детали реализации, но тем не менее очень помогающие в понимании). Я с такой точки зрения не смотрел даже. Спасибо.
                              • 0
                                добавлю, пожалуй, эти пару абзацев примечанием в статью после этого примера чуть позже
                              • +6
                                Это только кажется, что определенный. Компилятор может написать (псевдоассмеблер)
                                // i = -1;
                                xor i, i
                                sub i, 1
                                

                                И упорядочить при вызове функции инстуркции так:

                                xor i, i
                                xor i, i
                                sub i, 1
                                sub i, 1
                                call f(i, i)
                                
                            • 0
                              Правильно ли я понимаю, что выражение i * i++ приводит к неопределённому поведению по той причине, что side effect от инкремента начинает действовать сразу после входа в ветвь вычисления правого операнда, а не после вычисления всего выражения? И выражение при начальном i = 5 может дать результат как 25 (если компилятор решит сначала просчитать левую ветвь), так и 30 (если правую)?
                              • +1
                                Это логика, которой руководствовались разработчики стандарта, да. Но раз уж они включили подобное поведение в соответствующий список, то дальше это значит уже совсем другое.

                                Не пытайтесь «додумать» за компилятор логику обработки неопределённых поведений. Её нету. А есть только указание на то, что данный конкретный код никогда не будет выполняться с такими значениями, с которыми он мог бы вызвать неопределённое поведение. Сможет ли из этого знания компилятор извлечь какую-нибудь пользу и удалить, скажем,
                                if (abs(x) < 0) { /* обработаем случай когда x == MIN_INT */ }
                                — зависит от компилятора. Но не думайте что если сегодня, сейчас, ваш компилятор до этого не додумался, то и следующая, улучшенная версия тоже этого не сможет сделать. Очень может быть что сможет — и вам придётся долго и упорно сидеть в отладчике и офигевать от происходящего.
                              • 0
                                Спасибо, я как раз по этому собирал информацию (как профессионал, уточнял знакомому преподу лекцию).
                                • +2
                                  Говоря о неопределённом поведении, стоит упомянуть о разновидностях плохого кода, ведущего к UB.

                                  1. Неопределённость времени компиляции.
                                  Например, берём любую сомнительную арифметику (например, (-1)>>1 или 1<<100) и используем как константы времени компиляции.
                                  Нарушение ODR, опять же.

                                  2. Низкоуровневые проблемы, связанные с нарушением алиасинга и гонками чтения-записи — всё тот же i++ + i++

                                  3. Огромный класс проблем с неинициализированными и повреждёнными переменными (включая гонки конструкторов статических объектов, нулевые указатели, кривая адресная арифметика, стрельба по памяти)

                                  4. Нарушение контрактов (например, std::map с компаратором, не удовлетворяющим аксиоматике строгого порядка, или memcpy перехлёстывающихся диапазонов).

                                  5. Многопоточные гонки.
                                  • 0
                                    А где нибудь есть список всех случаев UB со ссылками на стандарт?
                                    Может от самого комитета?
                                    Например:
                                    — Изменение скалярного объекта между двумя точками следования более одного раза
                                    — разыменование нулевого указателя
                                    — переполнение знакового целочисленного
                                    — Использование неинициализированной переменной.
                                    и т.п.

                                    Это en.cppreference.com/w/cpp/language/ub полный список?
                                    И причём там «Infinite loop without side-effects»?
                                    • 0
                                      А где нибудь есть список всех случаев UB со ссылками на стандарт?
                                      У C есть приложение J.2, где есть ссылки на все другие разделы стандарта, где описаны все случаи, которые стандарт помещает в этот раздел. У C++ вроде бы нету, нужно искать в стандарте по слову «undefined» (и то можно что-нибудь пропустить).

                                      У меня есть мечта сделать подробное описание «с ментальной моделью» для каждого из них. Скажем проблемы алиасинга легко преставить себе вспомнив об 8087 или 3167, которые «висели» как независимые устройства на шине и потому попытка записать что-то в память как float с последующим чтением как int требовала явной операции синхронизации…

                                      Это http://en.cppreference.com/w/cpp/language/ub полный список?
                                      Смеётесь, что ли? Полный список — это сотни случаев разных, а там перечислены хорошо если пара десятков.

                                      Просто 90% (а то и 99%) всех UB споров не вызыват, все и так понимают, что «так делать низззя». Простейший пример: вызов функций с «…» и без «…» могут быть устроены радикально по разному и если вы будете вызывать функцию с «…» не включив заголовочный файл с соответствующим прототипом, то у вас программа может легко «слететь с катушек». И никто по этому поводу особо не ноет.

                                      И причём там «Infinite loop without side-effects»?
                                      Об этом явно написано в стандарте:
                                      4.7.2 Forward progress [intro.progress]
                                      1 The implementation may assume that any thread will eventually do one of the following:
                                      (1.1) — terminate,
                                      (1.2) — make a call to a library I/O function,
                                      (1.3) — perform an access through a volatile glvalue, or
                                      (1.4) — perform a synchronization operation or an atomic operation.
                                      [ Note: This is intended to allow compiler transformations such as removal of empty loops, even when termination cannot be proven. — end note ]
                                      То есть компилятор имеет право выкидывать циклы, которые «ничего не делают». Что-нибудь типа
                                        for (int i=0;i<1000000;i++)
                                          ; // Do nothing
                                      
                                      Можно просто извести. И такое:
                                        for (;;)
                                          ; // Do nothing
                                      
                                      Тоже. Правда и GCC и clang распознают несколько наиболее распространённых циклов и их оставляют, но делать это, строго говоря, не обязаны…

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

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