Есть ли утверждение 'int val = (++i> ++j)? ++i: ++j; ' вызвать неопределенное поведение?

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

#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 выглядит так, как будто она может скрывать какое-то неопределенное поведение, но я не вижу какой-либо точки, в которой объект либо модифицируется более одного раза, либо модифицируется и используется без промежуточной точки. Может ли кто-нибудь исправить или подтвердить мне это?

Ответ 1

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

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

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

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

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

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

Ответ 2

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

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

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

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

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

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

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

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

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

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

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

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

Ответ 3

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

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

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

Ответ 4

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

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

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

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

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

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

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

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