Какая из следующих комбинаций операторов post & pre-increment имеет поведение undefined в C?

Я читал, Может ли кто-нибудь объяснить эти поведения undefined (i = я ++ + ++ i, я = я ++ и т.д.) и попытался понимание Последовательности точек на "comp.lang.c FAQ" после траты более 2 часов времени, пытаясь объяснить следующие результаты gcc-компилятором.

expression(i=1;j=2)     i       j       k
k = i++ + j++;          2       3       3
k = i++ + ++j;          2       3       4
k = ++i + j++;          2       3       4
k = ++i + ++j;          2       3       5

k = i++ + i++;          3               2
k = i++ + ++i;          3               4
k = ++i + i++;          3               4
k = ++i + ++i;          3               6

i = i++ + j++;          4       3
i = i++ + ++j;          5       3
i = ++i + j++;          4       3
i = ++i + ++j;          5       3

i = i++ + i++;          4
i = i++ + ++i;          5
i = ++i + i++;          5
i = ++i + ++i;          6

Вопрос:

  • Я хочу знать, имеют ли все показанные выражения (в 4 группах) на приведенном выше рисунке поведение undefined? Если только некоторые из них имеют поведение undefined, какие из них выполняются, а какие - нет?

  • Для определенных выражений поведения, любезно, вы можете показать (не объяснять), как компилятор их оценивает. Просто, чтобы убедиться, что если я получил это предварительное приращение и post increment правильно.

Фон:

Сегодня я посетил интервью в кампусе, в котором меня попросили объяснить результаты i++ + ++i для заданного значения i. После компиляции этого выражения в gcc я понял, что ответ, который я дал в интервью, был неправильным. Я решил не делать такую ​​ошибку в будущем и, следовательно, попытался собрать все возможные комбинации операторов pre и post increment и скомпилировать их в gcc, а затем попытаться объяснить результаты. Я боролся более двух часов. Я не мог найти одно поведение оценки этих выражений. Итак, я сдался и повернулся к stackoverflow. После небольшого количества архивов чтения выяснилось, что есть что-то вроде поведения sequence point и undefined.

Ответ 1

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

Как определяется определенное поведение (группа 1):

i=1, j=2;

k=i++ + j++; // 1 + 2 = 3
k=i++ + ++j; // 1 + 3 = 4
k=++i + ++j; // 2 + 3 = 5
k=++i + j++; // 2 + 2 = 4

Это довольно прямолинейно. post-increment vs pre-increment thing.

В группах 2 и 4 довольно легко увидеть поведение undefined.

Группа 2 имеет поведение undefined, потому что оператор = не вводит точку последовательности.

Ответ 2

В любом из этих утверждений нет точек последовательности. Между ними есть точки последовательности.

Если вы дважды изменяете один и тот же объект между последовательными точками последовательности (в этом случае либо через =, либо через префикс или постфикс ++), поведение undefined. Таким образом, поведение первой группы из 4 операторов хорошо определено; поведение остальных undefined.

Если поведение определено, то i++ дает предыдущее значение i, а в качестве побочного эффекта изменяет i, добавляя к нему 1. ++i изменяет i, добавляя к нему 1, а затем дает модифицированное значение.

Ответ 3

Я хочу знать, имеют ли все показанные выражения (в 4 группах) на приведенном выше рисунке поведение undefined?

Строки с 2 по 5:

k = i++ + j++;
k = i++ + ++j;
k = ++i + ++j;
k = ++i + j++;

все четко определены. Все остальные выражения undefined, потому что все они пытаются изменить значение объекта посредством оценки выражения более одного раза между точками последовательности (для этих примеров точка последовательности встречается на ";", заканчивая каждый оператор). Например, i = i++; - undefined, потому что мы пытаемся изменить значение i как для назначения, так и для постфикса ++ без промежуточной точки последовательности. Оператор FYI = не вводит точку последовательности. Операторы || && ?: и ,comma вводят точки последовательности

Для определенных выражений поведения, любезно, вы можете показать (не объяснять), как компилятор их оценивает.

Начнем с

k = i++ + j++;

Выражение a++ оценивает текущее значение a, а в какой-то момент перед следующей точкой последовательности a увеличивается на 1. Таким образом, логически оценка выглядит примерно так:

k = 1 + 2; // i++ evaluates to 1, j++ evaluates to 2
i = i + 1; // i is incremented and becomes 2
j = j + 1; // j is incremented and becomes 3

Однако...

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

mov j, r0        ; read the value of j into register r0
mov i, r1        ; read the value of i into register r1
add r0, r1, r2   ; add the contents of r0 to r1, store result to r2
mov r2, k        ; write result to k
inc r1           ; increment value of i
inc r0           ; increment value of j
mov r0, j        ; store result of j++
mov r1, i        ; store result of i++

НЕ ПРИНИМАЙТЕ ОГРАНИЧЕНИЕ ОГРАНИЧЕНИЙ АРИФМЕТИЧЕСКИХ ВЫРАЖЕНИЙ. НЕ ПРИНИМАЙТЕ, ЧТО ОПЕРАЦИИ ++ и -- ОБНОВЛЕНО ОБНОВЛЕНО ПОСЛЕ ОЦЕНКИ.

Из-за этого результат выражений типа i++ + ++i будет меняться в зависимости от компилятора, параметров компилятора и даже окружающего кода. Поведение осталось undefined, чтобы компилятор не требовал "делать правильную вещь", что бы это ни было. Вы получите результат, но это не обязательно будет результат, которого вы ожидаете, и он не будет согласован во всех платформах.

Глядя на

k = i++ + ++j;

логическая оценка

k = 1 + 3  // i++ evaluates to i (1), ++j evaluates to j + 1 (2 + 1 = 3)
i = i + 1
j = j + 1

Опять же, здесь возможен один порядок упорядочения операций:

mov j, r0
inc r0
mov i, r1
add r0, r1, r2
mov r2, k
mov r0, j
inc r1
mov r1, i

Или это может сделать что-то еще. Компилятор может свободно изменять порядок, в котором оцениваются отдельные выражения, если это приводит к более эффективному порядку операций (что, по моему мнению, почти наверняка не является).

Ответ 4

Определена первая группа. Все они увеличивают значения как i, так и j как побочный эффект до следующей точки последовательности, поэтому i остается равным 2 и j как 3. Кроме того, i++ оценивается как 1, ++i оценивается до 2, j++ оценивается до 2 и ++j оценивается до 3. Это означает, что первый присваивает 1 + 2 - k, второй присваивает 1 + 3 - k, третий присваивает 2 + 3 до k, а четвертый присваивает 2 + 2 - k.

Остальные - это поведение undefined. Во второй и третьей группах i дважды изменяется до точки последовательности; в четвертой группе i изменяется три раза до точки последовательности.

Ответ 5

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

С учетом кода:

void test(unsigned *a, unsigned *b, unsigned *c)
{
  (*a) = (*b)++ + (*c)++;
}

существует много разумных способов, которыми компилятор может обработать это. Это может загрузить b и c, добавить их, сохранить результат в a, а затем увеличить b и c, или он может загрузить a и b, вычислить a + b, a + 1 и b + 1, а затем записывать их в произвольной последовательности или выполнять любое из бесчисленных других последовательности операций. На некоторых процессорах могут быть некоторые механизмы более эффективны, чем другие, и у компилятора не должно быть никаких оснований ожидать что программисты рассматривают любую договоренность как более подходящую, чем любая другие.

Обратите внимание, что хотя на большинстве аппаратных платформ будет ограничен количество правдоподобных поведений, которые могут возникнуть в результате прохождения одинаковых указатели на a, b и c, авторы Стандарта не прилагают никаких усилий для отличать правдоподобные и неправдоподобные результаты. Хотя многие реализации могут легко при практически нулевых затратах предлагать некоторую поведенческую гарантию (например, гарантировать, что код, подобный приведенному выше, всегда будет устанавливать *a, *b и *c на некоторые, возможно, неспецифицированные значения без каких-либо других побочных эффектов) и даже хотя такая гарантия иногда может быть полезна (если указатели будут идентифицировать отдельные объекты в тех случаях, когда значения объектов имеют значение, но могут и не делать этого в противном случае), модным для авторов компилятора является рассмотрение любой небольшой возможности полезной оптимизации, которую они могли бы достичь, когда она была предоставлена карт-бланш для запуска произвольно-разрушительных побочных эффектов будет стоить больше, чем ценностные программисты могут получить от уверенности в ограниченном поведении.

Ответ 6

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

Так

int a=0;
function_call(++a, ++a, ++a);

может иметь непредсказуемые результаты.

Ответ 7

В большинстве случаев gcc сначала выполняет предварительные приращения и использует эти значения в операциях и после этого оценивает приращения постов.

Например. В блоке 2 Pre приращения нет, поэтому для i 1 используется

k = i++ + i++ // hence k = 1+1=2

И два пошаговых приращения в i, так что я = 3

Один предварительный приращение изменяет я на 2

k = i++ + ++i // hence k= 2+2= 4

Однострочное увеличение в i, поэтому i= 3

То же самое для k= ++i + i++

Два предварительных приращения в i делают его 3

k=++i + ++i // hence k=3+3= 6

И i = 3

Надеюсь, это немного объяснит. Но это зависит исключительно от компилятора.