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

988

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

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

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

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

(Примечание. Предполагается, что это будет вход в FAQ по C ++ в Stack Overflow . Если вы хотите критиковать идею предоставления FAQ в этой форме, то публикация в meta, с которой все это началось, будет подходящим местом для этого. Этот вопрос отслеживается в чате C ++ , где идея FAQ возникла в первую очередь, поэтому ваш ответ, скорее всего, будет прочитан теми, кто придумал эту идею.)

неизвестно.
источник

Ответы:

683

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 здесь .

Prasoon Saurav
источник
45
*p++ = 4 не является неопределенным поведением. *p++интерпретируется как *(p++). p++возвращается p(копия) и значение сохраняется по предыдущему адресу. Почему это вызывает UB? Это прекрасно.
Prasoon Saurav
7
@Mike: AFAIK, нет (легальных) копий C ++ Standard, на которые вы могли бы сослаться.
СБИ
11
Ну, тогда у вас может быть ссылка на соответствующую страницу заказа ISO. В любом случае, если подумать, фраза «элементарное знание стандарта C ++» кажется немного противоречивой в терминах, поскольку, если вы читаете стандарт, вы пройдете элементарный уровень. Может быть, мы могли бы перечислить, какие вещи в языке вам нужны для базового понимания, такие как синтаксис выражений, порядок операций и, возможно, перегрузка операторов?
Майк ДеСимоне,
41
Я не уверен, что цитирование стандарта - лучший способ научить новичков
Inverse
6
@Adrian Первое выражение вызывает UB, потому что между последним ++iи назначением отсутствует точка последовательности i. Второе выражение не вызывает UB, потому что выражение iне меняет значение i. Во втором примере перед i++последовательным ,оператором присваивания следует точка точки ( ).
Колюня
276

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


Пререквизиты : элементарные знания по связям (математика).


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

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

Последовательность Очки были заменены Sequenced Перед и Sequenced ПослеUnsequenced и неопределенно Sequenced ) отношений в C ++ 11.


Что именно это за последовательность?

Sequenced Before (§1.9 / 13) - это отношение, которое:

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

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

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

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


Что означает слово «оценка» в контексте C ++ 11?

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

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

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

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

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

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

    int x; x = 10; ++x;

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


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

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

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

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

Например :

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

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

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

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

Важнее

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

(а) еще один побочный эффект на тот же скалярный объект

или

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

поведение не определено .

Примеры:

int i = 5, v[10] = { };
void  f(int,  int);
  1. i = i++ * ++i; // Undefined Behaviour
  2. i = ++i + i++; // Undefined Behaviour
  3. i = ++i + ++i; // Undefined Behaviour
  4. i = v[i++]; // Undefined Behaviour
  5. i = v[++i]: // Well-defined Behavior
  6. i = i++ + 1; // Undefined Behaviour
  7. i = ++i + 1; // Well-defined Behaviour
  8. ++++i; // Well-defined Behaviour
  9. f(i = -1, i = -1); // Undefined Behaviour (see below)

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

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


Заключительное примечание :

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

Prasoon Saurav
источник
3
Вместо «асимметричных», последовательности до / после являются «антисимметричными» отношениями. Это должно быть изменено в тексте, чтобы соответствовать определению частичного порядка, данному позже (что также согласуется с Википедией).
TemplateRex
1
Почему 7) элемент в последнем примере UB? Может быть так и должно быть f(i = -1, i = 1)?
Михаил
1
Я исправил описание отношения "секвенированный до". Это строгий частичный порядок . Очевидно, что выражение нельзя упорядочить перед собой, поэтому отношение не может быть рефлексивным. Следовательно, он асимметричный, а не антисимметричный.
ThomasMcLeod
1
5) хорошее настроение взорвало мой разум. объяснение Йоханнеса Шауба было не совсем простым, чтобы получить. Тем более, что я полагал, что даже в ++i(будучи оцененным значением перед +оператором, который его использует), стандарт все еще не говорит, что его побочный эффект должен быть закончен. Но на самом деле, поскольку он возвращает ссылку на a, lvalueкоторый iсам по себе, он ДОЛЖЕН закончить побочный эффект, поскольку оценка должна быть завершена, поэтому значение должно быть актуальным. Это была сумасшедшая часть, чтобы получить на самом деле.
v.oddou
«Члены комитета ISO C ++ считали, что материалы Sequence Points довольно трудно понять. Поэтому они решили заменить его на вышеупомянутые отношения только для более четкой формулировки и повышения точности». - у вас есть ссылка на это требование? Мне кажется, что новые отношения сложнее понять.
ММ
30

C ++ 17 ( N4659) включает предложение « Уточнение порядка оценки выражений для Idiomatic C ++», в котором определяется более строгий порядок вычисления выражений.

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

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

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

вместе со следующим уточнением

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

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

a[++i] = i;

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

В 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

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

AlexD
источник
почему i = i++ + 1;определяется поведение в c ++ 17, я думаю, даже если «правый операнд секвенирован перед левым операндом», однако модификация «i ++» и побочный эффект для присваивания не секвенированы, пожалуйста, дайте больше подробностей для их интерпретации
Джек X
@jackX Я расширил ответ :).
AlexD
да, я думаю, что подробности интерпретации предложения "Правый операнд секвенируется перед левым операндом" более полезна. Так как "Правый операнд секвенируется перед левым операндом" означает вычисление значения и побочный эффект, связанный с правым операндом, секвенировали до левого операнда. как вы сделали :-)
Джек X
11

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

f (a,b)

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

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

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

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

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

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