C ++ 98 и C ++ 03
Этот ответ для более старых версий стандарта C ++. Версии стандарта на C ++ 11 и C ++ 14 формально не содержат «точек последовательности»; вместо этого операции «секвенируются до», «не секвенированы» или «неопределенно секвенированы». Чистый эффект по сути тот же, но терминология другая.
Отказ от ответственности : хорошо. Этот ответ немного длинный. Так что наберитесь терпения, читая его. Если вы уже знаете эти вещи, их повторное чтение не сойдет с ума.
Пререквизиты : элементарные знания C ++ Standard
Каковы Очки Последовательности?
Стандарт говорит
В определенных точках в последовательности выполнения, называемых точками последовательности , все побочные эффекты предыдущих оценок должны быть завершены, и никаких побочных эффектов последующих оценок не должно быть. (§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)
(здесь a, b - оператор запятой; in func(a,a++)
,
- не оператор запятой, это просто разделитель между аргументами a
и a++
. Таким образом, поведение в этом случае не определено (если a
считается типом примитива))
при вызове функции (независимо от того, является ли функция встроенной), после оценки всех аргументов функции (если таковые имеются), которая происходит перед выполнением любых выражений или операторов в теле функции ( §1.9/17
).
1: Примечание: оценка полного выражения может включать оценку подвыражений, которые не являются лексической частью полного выражения. Например, подвыражения, участвующие в оценке выражений аргумента по умолчанию (8.3.6), считаются созданными в выражении, которое вызывает функцию, а не в выражении, которое определяет аргумент по умолчанию.
2: Указанные операторы являются встроенными операторами, как описано в разделе 5. Когда один из этих операторов перегружен (раздел 13) в допустимом контексте, что обозначает пользовательскую функцию оператора, выражение обозначает вызов функции и операнды формируют список аргументов, без подразумеваемой точки последовательности между ними.
Что такое неопределенное поведение?
Стандарт определяет неопределенное поведение в разделе §1.3.12
как
поведение, которое может возникнуть при использовании ошибочной программной конструкции или ошибочных данных, к которым настоящий международный стандарт не предъявляет никаких требований 3 .
Неопределенное поведение также может ожидаться, когда в этом международном стандарте опущено описание любого явного определения поведения.
3: допустимое неопределенное поведение варьируется от полного игнорирования ситуации с непредсказуемыми результатами до поведения во время перевода или выполнения программы документированным образом, характерным для среды (с выдачей диагностического сообщения или без него), до прекращения перевода или выполнения (с выдачей диагностического сообщения).
Короче говоря, неопределенное поведение означает, что может произойти все, что угодно: от демонов, вылетающих из носа, до беременности вашей подруги.
Какова связь между неопределенным поведением и точками последовательности?
Прежде чем я углублюсь в это, вы должны знать разницу между неопределенным поведением, неопределенным поведением и реализацией, определяемой реализацией .
Вы также должны это знать 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's 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 = 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 (что происходит в i++
), и поэтому нет хорошего способа определить - ни для нашего понимания, ни для компилятор - должен ли доступ осуществляться до или после сохранения увеличенного значения. Таким образом, поведение не определено.
Пример 3:
int x = i + i++ ;// Similar to above
Последующий ответ для C ++ 11 здесь .
*p++ = 4
не является неопределенным поведением.*p++
интерпретируется как*(p++)
.p++
возвращаетсяp
(копия) и значение сохраняется по предыдущему адресу. Почему это вызывает UB? Это прекрасно.