Что сделало i = i++ + 1; законным в C++17?


прежде чем вы начнете кричать неопределенное поведение, это явно перечислены в N4659 (C++17)

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

еще в N3337 (C++11)

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

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

из того, что я могу собрать, из [N4659 основных.старпома]

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

здесь стоимостью определена в [N4659 основной.тип]

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

С [N3337 основных.старпома]

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

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

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

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

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

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

С [N4659 выражение.осел]

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

С [N3337 выражение.осел]

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

единственная разница в том, что последнее предложение отсутствует в N3337.

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

3 162

3 ответа:

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

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

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

правый операнд секвенируется перед левым операндом.

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

(обновлено с учетом комментариев @John Bollinger.)

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

правый операнд секвенируется перед левым операндом.

и вы правильно определили, что оценка левого операнда как lvalue не имеет значения. Однако,sequenced before задано транзитивное отношение. Поэтому полный правый операнд (включая постинкремент) равен и последовательность перед назначением. В C++11, только значение вычисление правый операнд был секвенирован перед назначением.

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

оценки операндов не имеют последовательности.

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

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

правый операнд секвенируется перед левым операндом.


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

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

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