Что сделало я = я ++ + 1; юридический в С++ 17?

Прежде чем вы начнете вопить поведение undefined, это явно указано в N4659 (С++ 17)

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

Тем не менее в N3337 (С++ 11)

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

Что изменилось?

Из того, что я могу собрать, из [N4659 basic.exec]

За исключением тех случаев, когда отмечено, оценки операндов отдельных операторов и подвыражений отдельных выражений не имеют никакого значения. [...] Вычисления значений операндов оператора секвенируются перед вычислением значения результата оператора. Если побочный эффект на ячейке памяти не зависит от другого побочного эффекта в той же ячейке памяти или вычисления значения, используя значение любого объекта в том же месте памяти, и они не являются потенциально параллельными, поведение undefined.

Где значение определено на [N4659 basic.type]

Для тривиально копируемых типов представление значений представляет собой набор битов в представлении объекта, который определяет значение, которое является одним дискретным элементом определенного для реализации набора значений

От [N3337 basic.exec]

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

Аналогично, значение определяется в [N3337 basic.type]

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

Они идентичны, кроме упоминания concurrency, что не имеет значения, и с использованием места памяти вместо скалярного объекта, где

Арифметические типы, типы перечисления, типы указателей, указатели на типы членов, std::nullptr_t и cv-квалификационные версии этих типов в совокупности называются скалярными типами.

Это не влияет на пример.

Из [N4659 expr.ass]

Оператор присваивания (=) и составные операторы присваивания все группы справа налево. Все они требуют модифицируемого lvalue в качестве своего левого операнда и возвращают lvalue, ссылаясь на левый операнд. Результатом во всех случаях является бит-поле, если левым операндом является бит-поле. Во всех случаях назначение упорядочивается после вычисления значения правого и левого операндов и перед вычислением значения выражения присваивания. Правый операнд секвенирован перед левым операндом.

От [N3337 expr.ass]

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

Единственное отличие в том, что последнее предложение отсутствует в N3337.

Последнее предложение, однако, не должно иметь никакого значения, поскольку левый операнд i не является ни "другим побочным эффектом", ни "использованием значения одного и того же скалярного объекта", поскольку id-выражение является lvalue.

Ответ 1

В С++ 11 акт "присвоения", то есть побочный эффект модификации LHS, секвенируется после вычисления значения правого операнда. Обратите внимание, что это относительно "слабая" гарантия: она создает последовательность только по отношению к вычислению стоимости RHS. Он ничего не говорит о побочных эффектах, которые могут присутствовать в RHS, поскольку появление побочных эффектов не является частью вычисления стоимости. Требования С++ 11 не устанавливают относительного упорядочения между актом присвоения и любыми побочными эффектами RHS. Именно это создает потенциал для UB.

Единственная надежда в этом случае - любые дополнительные гарантии, сделанные конкретными операторами, используемыми в RHS. Если RHS использовал префикс ++, свойства секвенирования, специфичные для формы префикса ++, сохранили бы день в этом примере. Но postfix ++ - это совсем другая история: он не дает таких гарантий. В С++ 11 побочные эффекты = и postfix ++ в этом примере не подвержены влиянию отношения друг к другу. И это UB.

В С++ 17 добавляется дополнительное предложение к спецификации оператора присваивания:

Правильный операнд секвенирован перед левым операндом.

В сочетании с вышесказанным это дает очень сильную гарантию. Он выполняет все, что происходит в RHS (включая любые побочные эффекты), перед тем, что происходит в LHS. Поскольку фактическое присваивание секвенируется после LHS (и RHS), это дополнительное секвенирование полностью изолирует акт назначения от любых побочных эффектов, присутствующих в RHS. Это более сильное упорядочение - это то, что устраняет вышеуказанный UB.

(Обновлено, чтобы принимать во внимание комментарии @John Bollinger.)

Ответ 2

Вы определили новое предложение

Правильный операнд секвенирован перед левым операндом.

и вы правильно определили, что оценка левого операнда как lvalue не имеет значения. Тем не менее, секвенированные ранее определены как транзитивные отношения. Таким образом, полный правый операнд (включая пост-инкремент) также секвенирован перед назначением. В С++ 11 только вычисление значения правого операнда было упорядочено до назначения.

Ответ 3

В старых С++-стандартах и ​​в C11 определение текста оператора присваивания заканчивается текстом:

Оценки операндов не подвержены.

Значение того, что побочные эффекты в операндах не подвержены последовательности и, следовательно, определенно поведение undefined, если они используют одну и ту же переменную.

Этот текст был просто удален на С++ 11, оставив его несколько неоднозначным. Это UB, или нет? Это было выяснено в С++ 17, где они добавили:

Правильный операнд секвенирован перед левым операндом.


В качестве дополнительной заметки, в еще более старых стандартах, все это было очень ясно, например, с C99:

Порядок оценки операндов не определен. Если делается попытка изменить результат оператора присваивания или доступа к нему после следующей точки последовательности, поведение undefined.

В основном, в C11/С++ 11, они перепутались, когда они удалили этот текст.