Имеет ли выражение `int val = (++ i> ++ j)? ++ i: ++ j; `вызвать неопределенное поведение? - PullRequest
24 голосов
/ 14 марта 2019

Учитывая следующую программу:

#include <stdio.h>
int main(void)
{
    int i = 1, j = 2;
    int val = (++i > ++j) ? ++i : ++j;
    printf("%d\n", val); // prints 4
    return 0;
}

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

Ответы [ 4 ]

37 голосов
/ 14 марта 2019

Поведение этого кода хорошо определено.

Первое выражение в условном выражении гарантированно будет оценено перед вторым или третьим выражением, и будет оцениваться только одно из второго или третьего выражения.Это описано в разделе 6.5.15p4 C стандарта :

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

В случае вашего выражения:

int val = (++i > ++j) ? ++i : ++j;

++i > ++j оценивается первым.Приращенные значения i и j используются в сравнении, поэтому оно становится 2 > 3.Результат ложный, поэтому ++j оценивается, а ++i - нет.Таким образом, (снова) увеличенное значение j (т.е. 4) затем присваивается val.

8 голосов
/ 14 марта 2019

слишком поздно, но, возможно, полезно.

(++i > ++j) ? ++i : ++j;

В документе ISO/IEC 9899:201xAnnex C(informative)Sequence points мы находим, что есть точка последовательности

Между оценками первого операнда условного оператора?: И любым вторым и третьим операндами вычисляется

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

По вашему выражению, единственный конфликт, который может возникнуть, будет между первым и вторым ++i или ++j.

В каждой точке последовательности значение, сохраненное последним в объекте, должно соответствовать значению, предписанному абстрактной машиной (это то, что вы вычислили бы на бумаге, как на машине Тьюринга).

Цитата от 5.1.2.3p3 Program execution

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

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

Например. i = i++. Поскольку ни один из операторов, участвующих в этом выражении, не представляет точки последовательности, вы можете переставлять выражения, которые являются побочными эффектами, как вы хотите. Язык C позволяет использовать любую из этих последовательностей

i = i; i = i+1; или i = i+1; i=i; или tmp=i; i = i+1 ; i = tmp; или tmp=i; i = tmp; i = i+1; или что-либо, что дает тот же результат, что и абстрактная семантика вычислений запрашивает интерпретацию этого вычисления. Стандарт ISO9899 определяет язык C как абстрактную семантику.

5 голосов
/ 14 марта 2019

Возможно, в вашей программе нет UB, но в вопросе: вызывает ли оператор int val = (++i > ++j) ? ++i : ++j; неопределенное поведение?

Ответ - да.Любая или обе операции приращения могут быть переполнены, так как i и j подписаны, и в этом случае все ставки выключены.

Конечно, это не происходит в вашем полном примере, потому что выуказаны значения в виде маленьких целых чисел.

0 голосов
/ 15 марта 2019

Я собирался прокомментировать @Doug Currie, что целочисленное переполнение со знаком было слишком далеко, хотя технически правильный как ответ.Наоборот!

Подумав еще раз, я думаю, что ответ Дуга не только правильный, но предполагается, что не совсем тривиальный трехслойный, как в примере (но программа с, возможно, циклом или чем-то подобным) должна бытьраспространено на четкое, определенное «да».Вот почему:

Компилятор видит int i = 1, j = 2;, поэтому знает , что ++ i будет равен j и, следовательно, не может быть больше j или даже ++j.Современные оптимизаторы видят такие тривиальные вещи.

Если, конечно, один из них не переполнен.Но оптимизатор знает, что это будет UB, и поэтому предполагает, что и оптимизирует в соответствии с , этого никогда не произойдет .

Таким образом, условие троичного оператора всегда ложно (в этом простомпример, конечно, но даже если его несколько раз вызывать в цикле, это будет иметь место!), и i будет увеличиваться только один раз , тогда как j всегда будет увеличиваться в два раза .Таким образом, j не только всегда больше, чем i, но даже выигрывает на каждой итерации (пока не произойдет переполнение, но это никогда не произойдет в нашем предположении).

Таким образом, оптимизаторразрешено превратить это в ++i; j += 2; безоговорочно, что, безусловно, не то, что можно было бы ожидать.

То же самое относится, например, к циклу с unknown значениями i и j, например, предоставленный пользователем ввод.Оптимизатор может очень хорошо признать, что последовательность операций зависит только от начальных значений i и j.Таким образом, последовательность приращений, сопровождаемых условным перемещением, может быть оптимизирована путем дублирования цикла, по одному разу для каждого случая, и переключения между ними одним if(i>j).И затем, пока мы на нем, он может свернуть цикл повторяющихся приращений на два в нечто вроде (j-i)<<1, которое он просто добавляет.Или что-то в этом роде.
В предположении, что переполнения никогда не произойдет - это предположение, что оптимизатору разрешено делать, а делает - такое изменение, которое может полностью изменить весь смысл и режим.работы программы совершенно нормально.

Попробуйте и отладьте это.

...