Что сделало я = я ++ + 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, они перепутались, когда они удалили этот текст.