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

186

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

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

Еще в N3337 (C ++ 11)

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

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

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

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

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

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

Из [N3337 basic.exec]

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

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

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

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

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

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

От [N4659 expr.ass]

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

От [N3337 expr.ass]

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

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

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

Прохожий
источник
23
Вы определили причину: в C ++ 17 правый операнд упорядочен перед левым операндом. В C ++ 11 такой последовательности не было. Какой именно ваш вопрос?
Robᵩ
4
@ Robᵩ Смотрите последнее предложение.
Прохожий
7
У кого-нибудь есть ссылка на мотивацию для этого изменения? Мне бы хотелось, чтобы статический анализатор мог сказать «вы не хотите этого делать», когда сталкиваетесь с подобным кодом i = i++ + 1;.
7
@NeilButterworth, это из статьи p0145r3.pdf : « Уточнение порядка оценки выражений для Idiomatic C ++».
xaizek
9
@NeilButterworth, раздел номер 2 говорит, что это противоречит интуиции, и даже эксперты не всегда поступают правильно во всех случаях. Это в значительной степени вся их мотивация.
xaizek

Ответы:

144

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

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

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

Правый операнд упорядочен перед левым операндом.

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

(Обновлено с учетом комментариев @Джона Боллинджера.)

Муравей
источник
3
Действительно ли правильно включать «фактическое назначение» в эффекты, охватываемые «левым операндом» в этом отрывке? Стандарт имеет отдельный язык о последовательности фактического назначения. Я возьму выдержку, которую вы представили, чтобы ограничить ее по объему секвенированием левых и правых подвыражений, что в сочетании с остальной частью этого раздела недостаточно для поддержки определенность высказывания ОП.
Джон Боллинджер
11
Исправление: фактическое назначение все еще упорядочено после вычисления значения левого операнда, а вычисление левого операнда упорядочено после (полного) вычисления правого операнда, поэтому да, это изменение достаточно для поддержки четкости определения ОП спросил о. Тогда я просто прикалываюсь о деталях, но они имеют значение, поскольку они могут иметь разное значение для разного кода.
Джон Боллинджер
3
@JohnBollinger: мне кажется любопытным, что авторы Стандарта внесли бы изменения, которые ухудшают эффективность даже простой генерации кода и исторически не были необходимы, и все же возражают против определения других способов поведения, отсутствие которых является гораздо более серьезной проблемой, и которые будет редко представлять какое-либо значимое препятствие для эффективности.
суперкат
1
@Kaz: для составных назначений выполнение оценки значения слева после правой стороны позволяет x -= y;обрабатывать что-то вроде, mov eax,[y] / sub [x],eaxа не mov eax,[x] / neg eax / add eax,[y] / mov [x],eax. Я не вижу в этом ничего идиотского. Если нужно было указать порядок, наиболее эффективный порядок, вероятно, заключался бы в том, чтобы сначала выполнить все вычисления, необходимые для идентификации левого объекта, а затем оценить правый операнд, а затем значение левого объекта, но для этого потребовалось бы иметь термин для акта разрешения идентификатора левого объекта.
суперкат
1
@ Kaz: Если бы xи yбыло volatile, это имело бы побочные эффекты. Кроме того, те же соображения будут применяться к x += f();, где f()изменяется x.
суперкат
33

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

Правый операнд упорядочен перед левым операндом.

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


источник
7

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

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

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

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

Правый операнд упорядочен перед левым операндом.


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

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

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

Лундин
источник
1

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

Объяснение в других ответах является правильным и также относится к следующему коду, который теперь четко определен (и не меняет хранимое значение i):

i = i++;

Это + 1красная сельдь, и не совсем понятно, почему Стандарт использовал ее в своих примерах, хотя я помню, что люди спорили в списках рассылки до C ++ 11, что, возможно, это + 1имело значение из-за принудительного раннего преобразования lvalue справа. сторона Конечно, ничего из этого не применимо в C ++ 17 (и, вероятно, никогда не применяется ни в одной версии C ++).

М.М.
источник