В каких версиях стандарта С++ "(i + = 10) + = 10" имеют поведение undefined?

В С++ выполняется следующее поведение undefined:

int i = 0;
(i+=10)+=10;

В комментариях к моему ответу был обсужден вопрос Что такое результат + = в C и С++? Тонкость здесь заключается в том, что ответ по умолчанию кажется "да", тогда как кажется, что правильный ответ "зависит от версии стандарта С++".

Если это зависит от версии стандарта, объясните, где находится UB, а где нет.

Ответ 1

tl; dr: последовательность изменений и чтений, выполненных в (i+=10)+=10, хорошо определена как в С++ 98, так и в С++ 11, однако в С++ 98 это не достаточно, чтобы определить поведение.

В С++ 98 несколько модификаций одного и того же объекта без промежуточной последовательности указывают на поведение undefined, даже если порядок этих изменений хорошо определен. Это выражение не содержит точек последовательности, и поэтому факт, что он состоит из двух модификаций, достаточен для отображения его поведения undefined.

С++ 11 не имеет точек последовательности и требует только того, чтобы модификации объекта упорядочивались друг относительно друга и читали один и тот же объект для создания определенного поведения.

Поэтому поведение undefined в С++ 98, но хорошо определенное в С++ 11.


С++ 98

Предложение С++ 98 [expr] 5 p4

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

Предложение С++ 98 [expr.ass] 5.17 p1

Результатом операции присваивания является значение, сохраненное в левом операнде после выполнения присвоения; результатом является lvalue

Итак, я считаю, что порядок указан, однако я не вижу, что этого достаточно, чтобы создать точку последовательности в середине выражения. И продолжаем с цитатой [expr] 5 p4:

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

Итак, хотя порядок указан, мне кажется, что этого недостаточно для определенного поведения в С++ 98.


С++ 11

С++ 11 удаляет точки последовательности для более четкого представления о последовательности-before и sequenced-after. Язык из С++ 98 заменяется на

С++ 11 [intro.execution] 1.9 p15

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

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

С++ 11 [expr.ass] 5.17 p1

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

Так что, будучи упорядоченным, было недостаточно для того, чтобы сделать поведение, определенное в С++ 98, С++ 11 изменило требование, чтобы быть упорядоченным (то есть последовательным) достаточно.

(И мне кажется, что дополнительная гибкость, обеспечиваемая "sequence before" и "sequenced after", привела к гораздо более четкому, согласованному и хорошо определенному языку.)


Мне кажется маловероятным, что любая реализация на С++ 98 действительно сделала бы что-нибудь удивительное, если последовательность операций будет хорошо указана, даже если этого недостаточно для получения технически корректного поведения. В качестве примера, внутреннее представление этого выражения, созданного Clang в режиме С++ 98, имеет четко определенное поведение и делает ожидаемую вещь.

Ответ 2

В С++ 11 выражение хорошо определено и приведет к i == 20.

От [expr.ass]/1:

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

Это означает, что присваивание i+=1 секвенировано перед вычислением значения левой стороны (i+=10)+=10, которое, в свою очередь, упорядочивается перед окончательным назначением на i.


В С++ 03 выражение имеет поведение undefined, поскольку оно вызывает изменение i дважды без промежуточной точки последовательности.

Ответ 3

Может быть. Это зависит от версии С++.

В С++ 03 это очевидный UB, между присваиваниями нет промежуточной точки последовательности.

В С++ 11, как объясняет Манкарсе, это не undefined больше - назначение в виде скобок указано в последовательности перед внешним, поэтому все в порядке.