Есть ли утверждение '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
которое он просто добавляет. Или что-то.
В предположении, что переполнения никогда не происходит - то есть допущении, что оптимизатору разрешено и действительно выполняется - такая модификация, которая может полностью изменить весь смысл и режим работы программы, вполне подойдет.
Попробуйте и отладьте это.