Неопределенные точки поведения и последовательности

939

Что такое "точки последовательности"?

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

Я часто использую смешные и запутанные выражения типа a[++i] = i;, чтобы заставить себя чувствовать себя лучше. Почему я должен прекратить их использовать?

Если вы прочитали это, обязательно посетите следующий вопрос Undefined поведение и точки последовательности, которые перезагружены.

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

Теги:
undefined-behavior
sequence-points
c++-faq

6 ответов

617
Лучший ответ

C++ 98 и C++ 03

Этот ответ предназначен для более старых версий стандарта C++. Версии стандарта C++ 11 и C++ 14 формально не содержат "точек последовательности"; вместо этого операции "секвенируются до", "не секвенированы" или "неопределенно секвенированы". Чистый эффект по сути тот же, но терминология другая.


Отказ от ответственности: хорошо. Этот ответ немного длинный. Так что наберитесь терпения, читая его. Если вы уже знаете эти вещи, их повторное чтение не сойдет с ума.

Предварительные условия: элементарное знание стандарта C++


Каковы Очки Последовательности?

Стандарт говорит

В определенных точках в последовательности выполнения, называемых точками последовательности, все побочные эффекты предыдущих оценок должны быть завершены, и никаких побочных эффектов последующих оценок не должно быть. (§1.9/7)

Побочные эффекты? Каковы побочные эффекты?

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

Например:

int x = y++; //where y is also an int

В дополнение к операции инициализации изменяется значение y из-за побочного эффекта оператора ++.

Все идет нормально. Переходя к точкам последовательности. Определение чередования seq-точек, данное автором comp.lang.c Steve Summit:

Точка последовательности - это момент времени, когда пыль осела, и все побочные эффекты, которые были замечены до сих пор, гарантированно будут завершены.


Какие общие точки последовательности перечислены в стандарте C++?

Это:

  • в конце оценки полного выражения (§1.9/16) (полное выражение - это выражение, которое не является подвыражением другого выражения.) 1

Пример:

int a = 5; // ; is a sequence point here
  • в оценке каждого из следующих выражений после оценки первого выражения (§1.9/18) 2

    • a && b (§5.14)
    • a || b (§5.15)
    • a? b: c (§5.16)
    • a, b (§5.18) (здесь а, Ь является оператором запятой, в func(a,a++) , не является оператором запятой, это просто разделитель между аргументами a и a++. Таким образом, поведение не определено в этом случае (если a считается примитивным типом)
  • при вызове функции (независимо от того, является ли функция встроенной), после оценки всех аргументов функции (если таковые имеются), которая происходит до выполнения каких-либо выражений или операторов в теле функции (§1.9/17).

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

2: указанные операторы являются встроенными операторами, как описано в разделе 5. Когда один из этих операторов перегружен (раздел 13) в допустимом контексте, обозначая, таким образом, определяемую пользователем функцию оператора, выражение обозначает вызов функции и операнды формируют список аргументов, без подразумеваемой точки последовательности между ними.


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

Стандарт определяет неопределенное поведение в §1.3.12 раздела как

поведение, которое может возникнуть при использовании ошибочной программной конструкции или ошибочных данных, к которым настоящий международный стандарт не предъявляет никаких требований 3.

Неопределенное поведение может также ожидаться, когда этот Международный стандарт опускает описание любого явного определения поведения.

3: допустимое неопределенное поведение варьируется от полного игнорирования ситуации с непредсказуемыми результатами до поведения во время перевода или выполнения программы документированным образом, характерным для среды (с выдачей диагностического сообщения или с with-), до прекращения перевода или выполнение (с выдачей диагностического сообщения).

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


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

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

Вы также должны знать, что the order of evaluation of operands of individual operators and subexpressions of individual expressions, and the order in which side effects take place, is unspecified.

Например:

int x = 5, y = 6;

int z = x++ + y++; //it is unspecified whether x++ or y++ will be evaluated first.

Еще один пример здесь.


Теперь Стандарт в §5/4 гласит:

  • 1) Между предыдущей и следующей точкой последовательности скалярный объект должен иметь свое сохраненное значение, измененное не более одного раза путем оценки выражения.

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

Неформально это означает, что между двумя точками последовательности переменная не должна изменяться более одного раза. В операторе выражения next sequence point обычно находится в завершающей точке с запятой, а previous sequence point - в конце предыдущего оператора. Выражение также может содержать промежуточные sequence points.

Из приведенного выше предложения следующие выражения вызывают неопределенное поведение:

i++ * ++i;   // UB, i is modified more than once btw two SPs
i = ++i;     // UB, same as above
++i = 2;     // UB, same as above
i = ++i + 1; // UB, same as above
++++++i;     // UB, parsed as (++(++(++i)))

i = (i, ++i, ++i); // UB, there no SP between '++i' (right most) and assignment to 'i' ('i' is modified more than once btw two SPs)

Но следующие выражения хороши:

i = (i, ++i, 1) + 1; // well defined (AFAIK)
i = (++i, i++, i);   // well defined 
int j = i;
j = (++i, i++, j*i); // well defined

  • 2) Кроме того, предварительное значение должно быть доступно только для определения значения, которое будет сохранено.

Что это значит? Это означает, что если объект записан в пределах полного выражения, любой и все обращения к нему в пределах одного и того же выражения должны быть непосредственно вовлечены в вычисление значения, которое будет записано.

Например, в i = я + 1 весь доступ i (в LHS и в RHS) непосредственно участвует в вычислении значения, которое должно быть записано. Так что все в порядке.

Это правило эффективно ограничивает юридические выражения теми, в которых доступы явно предшествуют модификации.

Пример 1:

std::printf("%d %d", i,++i); // invokes Undefined Behaviour because of Rule no 2

Пример 2:

a[i] = i++ // or a[++i] = i or a[i++] = ++i etc

запрещено, потому что один из обращений к i (тот, что в a[i]) не имеет никакого отношения к значению, которое в итоге сохраняется в я (что происходит в i++), и поэтому нет хорошего способа определить - либо для нашего понимания, либо для компилятора - должен ли доступ осуществляться до или после сохранения увеличенного значения. Так что поведение не определено.

Пример 3:

int x = i + i++ ;// Similar to above

Последующий ответ для C++ 11 здесь.

  • 1
    Я не понимаю часть «Более того». Разве это не делает постфикс ++ бесполезным? Например, в выражении *p++ = 4 к предыдущему значению p обращаются как для определения значения, которое должно быть сохранено в p (OK), так и для определения адреса, где хранить 4 (не OK?). Но, конечно, эта общая идиома не неопределенное поведение?
  • 45
    *p++ = 4 не является неопределенным поведением. *p++ интерпретируется как *(p++) . p++ возвращает p (копию) и значение сохраняется по предыдущему адресу. Почему это вызывает UB? Это прекрасно.
Показать ещё 30 комментариев
280

Это продолжение моего предыдущего ответа и содержит связанный с С++ 11 материал..


Предварительные требования: элементарное знание отношений (математика).


Верно ли, что в С++ 11 нет точек последовательности?

Да! Это очень верно.

Точки последовательности были заменены на Последовательность до и Последовательность после Непоследовательность и Неопределенно Sequenced) отношения в С++ 11.


Что именно происходит в этой статье?

Последовательность перед (§1.9/13) - это отношение, которое:

между оценками, выполненными одним thread и индуцирует строгий частичный порядок 1

Формально это означает, что для любых двух оценок (см. ниже)A и B, если A секвенировано до B, тогда выполнение of A должно предшествовать выполнению B. Если A не секвенируется до B, а B не секвенируется до A, то A и B непересекаются 2.

Оценки A и B являются неопределенно секвенированными, когда либо A секвенирован до B или B секвенирован до A, но не определено, какой > 3.

<суб > [ПРИМЕЧАНИЯ]суб > <Суб >
1: Строгий частичный порядок - это двоичное отношение "<" над множеством P, которое asymmetric и transitive, т.е. для всех A, B и c в P, мы имеем следующее:
........ (i). если a < b, затем ¬ (b < a) (asymmetry);........ (II). если a < b и b < c, тогда a < c (transitivity).
2: Выполнение неощутимых оценок может перекрываться.
3: Неопределенно упорядоченные оценки не могут пересекаться, но либо могут быть выполнены в первую очередь. Суб >


В чем смысл слова "оценка" в контексте С++ 11?

В С++ 11 оценка выражения (или подвыражения) в общем случае включает в себя:

  • вычисления значений (в том числе определение идентичности объекта для оценки glvalue и получение значения, ранее присвоенного объекту для оценка оценки) и

  • инициирование побочных эффектов.

Теперь (§1.9/14) говорится:

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

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

    int x;  x = 10;  ++x;

    Вычисление значения и побочный эффект, связанные с ++x, секвенируются после вычисления значения и побочного эффекта x = 10;


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

Да! Правильно.

В (§1.9/15) было отмечено, что

За исключением тех случаев, когда отмечено, оценки операндов отдельных операторов и подвыражений отдельных выражений не подвержены последовательности 4.

Например:

int main()
{
     int num = 19 ;
     num = (num << 3) + (num >> 3);
} 
  • Оценка операндов оператора + не зависит от друг друга.
  • Оценка операндов операторов << и >> не зависит друг от друга.

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

(§1.9/15) Вычисления значений операндов оператор упорядочивается перед вычислением значения результата оператора.

Это означает, что в x + y вычисление значения x и y секвенировано перед вычислением значения (x + y).

Более важно

(§1.9/15) Если побочный эффект на скалярном объекте не влияет на

(a) другой побочный эффект на том же скалярном объекте

или

(b) вычисление значения с использованием значения одного и того же скалярного объекта.

поведение undefined.

Примеры:

int i = 5, v[10] = { };
void  f(int,  int);
  • i = i++ * ++i; // Undefined Behaviour
  • i = ++i + i++; // Undefined Behaviour
  • i = ++i + ++i; // Undefined Behaviour
  • i = v[i++]; // Undefined Behaviour
  • i = v[++i]: // Well-defined Behavior
  • i = i++ + 1; // Undefined Behaviour
  • i = ++i + 1; // Well-defined Behaviour
  • ++++i; // Well-defined Behaviour
  • f(i = -1, i = -1); // Undefined Behaviour (see below)

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

Выражения (5), (7) и (8) не вызывают поведение Undefined. Ознакомьтесь с нижеследующими ответами для более подробного объяснения.


Заключительная записка:

Если вы обнаружите какой-либо недостаток в сообщении, оставьте комментарий. Power-users (С rep > 20000), пожалуйста, не стесняйтесь редактировать сообщение для исправления опечаток и других ошибок.

  • 3
    Вместо «асимметричных», секвенируются до / после «антисимметричных» отношений. Это должно быть изменено в тексте, чтобы соответствовать определению частичного порядка, данному позже (что также согласуется с Википедией).
  • 1
    Почему 7) элемент в последнем примере UB? Может быть, это должно быть f(i = -1, i = 1) ?
Показать ещё 10 комментариев
84

Точки последовательности

Точки последовательности - это точки в выполнении программы, в которой все побочные эффекты, полученные оценками до точек последовательности, были завершены. Побочные эффекты, возникающие в результате оценок, которые происходят после точки последовательности, будут отделяться от побочных эффектов, получаемых оценками, которые происходят до точки последовательности и происходят впоследствии.

Оценки

Оценка чего-то означает применение семантики выполнения во время выражения. Существуют неоцененные выражения (операнды sizeof, некоторые операнды typeid и т.д.), Которые проверяют только тип выражения и не имеют смысла во время выполнения. Если выражение оценивается, оно может привести к значению, которое может означать считывание значений из объектов, или оно может просто оценить объект, не читая его значение (тогда оно остается lvalue, как при левом подвыражении присвоения). Кроме того, при необходимости он может создавать побочные эффекты. Оценка завершена, если ее значение известно, но до тех пор, пока не будет достигнута точка последовательности, предполагается, что побочные эффекты, полученные в результате оценки, все еще обрабатываются.

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

  • После оценки a в a && b и a || b и a ? b : c. Также после оценки a в a, b - этот оператор называется "оператором запятой".
  • Для вызова функции, после оценки аргументов вызова функции и перед началом оценки в теле функции.
  • После оценки полного выражения (которое не было оценено как часть другого выражения). Примерами являются условия цикла, условия, значения переключателей и выражения.
  • Непосредственно перед завершением функции (путем разворачивания функции по исключению или путем обычного возврата ее после (возможно) создания возвращаемого значения). Это гарантирует, что каждый побочный эффект в функции действительно был урегулирован и полностью обработан.

Побочные эффекты

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

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

С этими тремя терминами поток программы можно визуализировать следующим образом. На следующих диаграммах a E(X) указывает оценку (под) выражения x, a % указывает точку последовательности, а S(k, e) указывает на побочный эффект k на объект e. Если оценка должна считывать значение из именованного объекта (если x - это имя), оценка записывается как V(x), в противном случае она записывается как E(X). Побочные эффекты записываются справа и слева в выражения. Ребро между двумя выражениями означает, что верхнее выражение оценивается перед нижним выражением (обычно потому, что нижнее выражение зависит от значения или 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 тела выполняются побочные эффекты, полученные путем оценки аргументов: для этого c и d были полностью увеличены.

Рассмотрим выражение выражение i++ * j++;

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

Ничего себе, откуда берутся две ветки? Помните из первоначального определения точки последовательности: точки последовательности влияют на оценки, которые происходят до. Все подвыражения умножения оцениваются до этого, и нет другой точки последовательности, поэтому мы должны предполагать "максимальную параллельность", чтобы найти, где потенциально мы соглашаемся писать на один и тот же объект. Формально две ветки не упорядочены. Отношение точки последовательности - это отношение, которое упорядочивает некоторые оценки друг другу и не заказывает других: для этого требуется частичный порядок.

Конфликтные побочные эффекты

Чтобы предоставить максимальную свободу компилятора при генерации и оптимизации машинного кода, такие случаи, как умножение выше, не упорядочивают оценки подвыражений и не разделяют побочные эффекты, производимые ими, за исключением нескольких случаев, описанных выше. Это может привести к конфликтам, и С++ Standard отмечает поведение программ undefined, если они пытаются изменить один и тот же объект без промежуточной точки последовательности (на самом деле он применяется к скалярным объектам, поскольку другие объекты либо немодифицируются (массивы) или просто не применимы к этому правилу (объекты класса)). Поведение также undefined, если предыдущее значение считывается с объекта, но есть также модификация, как в i * i++

// This yields to undefined behavior!
// Left 'i' is not guaranteed to read new value:

    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, если это необходимо для назначения a для получения нового значения из b. В спецификации это отношение устанавливается явными ограничениями, в этом случае оно, в частности, относится к b = 0 и читает "Результат операции присваивания - это значение, сохраненное в левом операнде после выполнения присвоения, lvalue". Почему бы не указать последовательность, чтобы сделать это отношение? Поскольку точка последовательности имела бы нежелательный эффект, требуя, чтобы каждый побочный эффект, который случается при оценке левого и правого операндов, был полным, вместо того чтобы делать это только для назначения в случае, если его полученное значение l считывается из.

Закрывающие слова

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

19

С++ 17 (N4659) включает предложение Обновление оценки оценки выражений для Idiomatic С++ который определяет более строгий порядок оценки выражения.

В частности, добавлено следующее предложение:

8.18 Операторы присваивания и составного присваивания:
....

Во всех случаях назначение упорядочивается после значения вычисление правого и левого операндов и перед вычислением значения выражения присваивания. Правильный операнд упорядочен перед левым операндом.

Он делает несколько случаев ранее допустимого поведения undefined, включая тот, о котором идет речь:

a[++i] = i;

Однако несколько других подобных случаев по-прежнему приводят к поведению undefined.

В N4140:

i = i++ + 1; // the behavior is undefined

Но в N4659

i = i++ + 1; // the value of i is incremented
i = i++ + i; // the behavior is undefined

Конечно, использование компилятора, совместимого с С++ 17, не обязательно означает, что нужно начинать писать такие выражения.

11

Я предполагаю, что есть фундаментальная причина изменения, это не просто косметика, чтобы сделать старую интерпретацию яснее: эта причина concurrency. Неопределенный порядок разработки - это просто выбор одного из нескольких возможных порядковых порядков, это совсем не так, как до и после заказов, потому что, если нет указанного порядка, возможна одновременная оценка: не так со старыми правилами. Например, в:

f (a,b)

ранее либо a, либо b, или b, то a. Теперь a и b можно оценить с чередованием инструкций или даже на разных ядрах.

  • 5
    Я полагаю, однако, что если или «a», или «b» включает в себя вызов функции, они являются неопределенными последовательностями, а не последовательностями, то есть все побочные эффекты одного из них должны возникать до появления любых побочных эффектов от другой, хотя компилятор не должен быть последовательным в отношении того, какой из них идет первым. Если это больше не так, это нарушило бы большую часть кода, который основывается на неперекрывающихся операциях (например, если каждый из «a» и «b» устанавливает, использует и снимает общее статическое состояние).
0

В C99(ISO/IEC 9899:TC3) который, по-видимому, отсутствует в этом обсуждении, до сих пор сделаны следующие steteents относительно порядка оценки.

[...] порядок вычисления подвыражений и порядок возникновения побочных эффектов не определены. (Раздел 6.5 с. 67)

Порядок оценки операндов не указан. Если предпринята попытка изменить результат оператора присваивания или получить к нему доступ после следующей точки последовательности, поведение [sic] не определено (Раздел 6.5.16, стр. 91).

  • 1
    Вопрос помечен C ++, а не C, что хорошо, потому что поведение в C ++ 17 весьма отличается от поведения в более старых версиях - и не имеет никакого отношения к поведению в C11, C99, C90 и т. Д. Или имеет очень мало отношение к этому. В целом, я бы предложил удалить это. Что еще более важно, нам нужно найти эквивалентные вопросы и ответы для C и убедиться, что все в порядке (и отмечает, что C ++ 17, в частности, меняет правила - поведение в C ++ 11 и ранее было более или менее таким же, как в C11, хотя словоблудие, описывающее его в C, все еще использует «точки последовательности», тогда как C ++ 11 и более поздние - нет.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню