Почему эти конструкции используют неопределенное поведение до и после приращения? - PullRequest
760 голосов
/ 04 июня 2009
#include <stdio.h>

int main(void)
{
   int i = 0;
   i = i++ + ++i;
   printf("%d\n", i); // 3

   i = 1;
   i = (i++);
   printf("%d\n", i); // 2 Should be 1, no ?

   volatile int u = 0;
   u = u++ + ++u;
   printf("%d\n", u); // 1

   u = 1;
   u = (u++);
   printf("%d\n", u); // 2 Should also be one, no ?

   register int v = 0;
   v = v++ + ++v;
   printf("%d\n", v); // 3 (Should be the same as u ?)

   int w = 0;
   printf("%d %d %d\n", w++, ++w, w); // shouldn't this print 0 2 2

   int x[2] = { 5, 8 }, y = 0;
   x[y] = y ++;
   printf("%d %d\n", x[0], x[1]); // shouldn't this print 0 8? or 5 0?
}

Ответы [ 14 ]

544 голосов
/ 04 июня 2009

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

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

Итак, с учетом этого, почему эти "проблемы"? Язык ясно говорит, что определенные вещи приводят к неопределенному поведению . Нет проблем, нет «надо» участвовать. Если неопределенное поведение изменяется, когда одна из задействованных переменных объявлена ​​volatile, это ничего не доказывает и не меняет. Это undefined ; Вы не можете рассуждать о поведении.

Ваш самый интересный пример, с

u = (u++);

- пример неопределенного поведения из учебника (см. Статью Википедии о точках последовательности ).

76 голосов
/ 24 мая 2010

Просто скомпилируйте и разберите вашу строку кода, если вы так хотите узнать, как именно вы получаете то, что получаете.

Это то, что я получаю на своей машине, вместе с тем, что я думаю:

$ cat evil.c
void evil(){
  int i = 0;
  i+= i++ + ++i;
}
$ gcc evil.c -c -o evil.bin
$ gdb evil.bin
(gdb) disassemble evil
Dump of assembler code for function evil:
   0x00000000 <+0>:   push   %ebp
   0x00000001 <+1>:   mov    %esp,%ebp
   0x00000003 <+3>:   sub    $0x10,%esp
   0x00000006 <+6>:   movl   $0x0,-0x4(%ebp)  // i = 0   i = 0
   0x0000000d <+13>:  addl   $0x1,-0x4(%ebp)  // i++     i = 1
   0x00000011 <+17>:  mov    -0x4(%ebp),%eax  // j = i   i = 1  j = 1
   0x00000014 <+20>:  add    %eax,%eax        // j += j  i = 1  j = 2
   0x00000016 <+22>:  add    %eax,-0x4(%ebp)  // i += j  i = 3
   0x00000019 <+25>:  addl   $0x1,-0x4(%ebp)  // i++     i = 4
   0x0000001d <+29>:  leave  
   0x0000001e <+30>:  ret
End of assembler dump.

(я ... полагаю, что инструкция 0x00000014 была какой-то оптимизацией компилятора?)

59 голосов
/ 04 июня 2009

Я думаю, что соответствующие части стандарта C99 - это 6,5 выражений, §2

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

и 6.5.16 Операторы присваивания, §4:

Порядок вычисления операндов не указан. Если попытка изменить результат оператора присваивания или для доступа к нему после следующей точки последовательности, поведение не определено.

49 голосов
/ 27 июня 2015

Большинство ответов здесь процитированы из стандарта C, подчеркивая, что поведение этих конструкций не определено. Чтобы понять , почему поведение этих конструкций не определено , давайте сначала разберемся в этих терминах в свете стандарта C11:

Последовательность: (5.1.2.3)

Учитывая любые две оценки A и B, если A секвенируется до B, тогда выполнение A должно предшествовать выполнению B.

Unsequenced:

Если A не упорядочено до или после B, то A и B не упорядочены.

Оценки могут быть одной из двух:

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

Точка последовательности:

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

Теперь перейдем к вопросу, для выражений типа

int i = 1;
i = i++;

Стандарт гласит:

6.5 Выражения:

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

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

Позволяет переименовать i слева от присваивания быть il и справа от присваивания (в выражении i++) быть ir, тогда выражение будет иметь вид

il = ir++     // Note that suffix l and r are used for the sake of clarity.
              // Both il and ir represents the same object.  

Важный момент относительно оператора Postfix ++ заключается в том, что:

только то, что ++ следует после переменной, не означает, что приращение происходит поздно . Приращение может произойти, как только компилятору понравится , если компилятор гарантирует, что используется исходное значение .

Это означает, что выражение il = ir++ может быть оценено как

temp = ir;      // i = 1
ir = ir + 1;    // i = 2   side effect by ++ before assignment
il = temp;      // i = 1   result is 1  

или

temp = ir;      // i = 1
il = temp;      // i = 1   side effect by assignment before ++
ir = ir + 1;    // i = 2   result is 2  

, что приводит к двум различным результатам 1 и 2, которые зависят от последовательности побочных эффектов при назначении и ++ и, следовательно, вызывают UB.

48 голосов
/ 15 августа 2013

Поведение на самом деле не может быть объяснено, потому что оно вызывает и неопределенное поведение и неопределенное поведение , поэтому мы не можем делать какие-либо общие прогнозы относительно этого кода, хотя, если вы прочитаете Работа Ольве Модала , такая как Deep C и Unspecified and Undefined иногда вы можете делать хорошие предположения в очень специфических случаях с определенным компилятором и средой, но, пожалуйста, не делайте этого что где-то рядом с производством.

Итак, переходя к неопределенное поведение , в черновик стандарта C99 раздел 6.5 абзац 3 говорит ( выделение шахты ):

Группировка операторов и операндов указывается синтаксисом.74) За исключением случаев, указанных позже (для функций function-call (), &&, ||,?: и запятых) порядок вычисления подвыражений и порядок возникновения побочных эффектов не определены.

Итак, когда у нас есть такая строка:

i = i++ + ++i;

мы не знаем, будет ли сначала оцениваться i++ или ++i. В основном это дает компилятору лучшие возможности для оптимизации .

У нас также есть неопределенное поведение и здесь, поскольку программа модифицирует переменные (i, u и т. Д.) Более одного раза между точками последовательности . Из проекта стандартного раздела 6.5 параграф 2 ( акцент шахты ):

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

он цитирует следующие примеры кода как неопределенные:

i = ++i + 1;
a[i++] = i; 

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

i = i++ + ++i;
^   ^       ^

i = (i++);
^    ^

u = u++ + ++u;
^   ^       ^

u = (u++);
^    ^

v = v++ + ++v;
^   ^       ^

Неуказанное поведение определяется в проекте стандарта c99 в разделе 3.4.4 как:

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

и неопределенное поведение определяется в разделе 3.4.3 как:

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

и отмечает, что:

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

31 голосов
/ 18 июня 2015

Другой способ ответить на этот вопрос, вместо того, чтобы увязнуть в таинственных деталях точек последовательности и неопределенного поведения, - просто спросить, что они должны означать? Что пытался программист делать?

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

Второй фрагмент, i = i++, немного легче понять. Кто-то явно пытается увеличить i и присвоить результат обратно i. Но есть несколько способов сделать это в C. Самый простой способ добавить 1 к i и присвоить результат обратно i, одинаков почти для любого языка программирования:

i = i + 1

C, конечно, имеет удобный ярлык:

i++

Это означает: «добавьте 1 к i и присвойте результат обратно i». Так что, если мы построим мешанину из двух, написав

i = i++

что мы на самом деле говорим: «добавьте 1 к i, присвойте результат обратно i и присвойте результат обратно i». Мы в замешательстве, поэтому меня не слишком беспокоит, если и компилятор тоже запутался.

На самом деле, эти сумасшедшие выражения пишутся только тогда, когда люди используют их в качестве искусственных примеров того, как ++ должен работать. И, конечно, важно понимать, как работает ++. Но одно практическое правило использования ++ таково: «Если неясно, что означает выражение с использованием ++, не пишите его».

Мы привыкли тратить бесчисленное количество часов на comp.lang.c, обсуждая подобные выражения и почему они не определены. Два моих более длинных ответа, которые пытаются действительно объяснить, почему, заархивированы в сети:

22 голосов
/ 30 декабря 2015

Часто этот вопрос связан как дубликат вопросов, связанных с кодом, например

printf("%d %d\n", i, i++);

или

printf("%d %d\n", ++i, i++);

или аналогичные варианты.

Хотя это также неопределенное поведение , как уже говорилось, существуют незначительные различия, когда printf() присутствует при сравнении с таким утверждением, как:

x = i++ + i++;

В следующем утверждении:

printf("%d %d\n", ++i, i++);

порядок оценки аргументов в printf() равен не указан . Это означает, что выражения i++ и ++i могут быть вычислены в любом порядке. C11 стандарт имеет несколько соответствующих описаний:

Приложение J, неопределенное поведение

Порядок, в котором указатель функции, аргументы и подвыражения в аргументах оцениваются в вызове функции (6.5.2.2).

3.4.4, неопределенное поведение

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

ПРИМЕР. Примером неопределенного поведения является порядок, в котором аргументы функции оцениваются.

неопределенное поведение само по себе НЕ является проблемой. Рассмотрим этот пример:

printf("%d %d\n", ++x, y++);

Это также имеет неопределенное поведение , потому что порядок оценки ++x и y++ не указан. Но это совершенно законное и обоснованное утверждение. В этом выражении есть нет неопределенного поведения. Поскольку изменения (++x и y++) выполняются для различных объектов.

Что делает следующее утверждение

printf("%d %d\n", ++i, i++);

как неопределенное поведение означает, что эти два выражения изменяют один и тот же объект i без промежуточной точки последовательности .


Другая деталь в том, что запятая , используемая в вызове printf (), является разделителем , а не оператором запятой .

Это важное различие, потому что оператор запятой вводит точку последовательности между оценкой их операндов, что делает следующее допустимым:

int i = 5;
int j;

j = (++i, i++);  // No undefined behaviour here because the comma operator 
                 // introduces a sequence point between '++i' and 'i++'

printf("i=%d j=%d\n",i, j); // prints: i=7 j=6

Оператор запятой оценивает свои операнды слева направо и выдает только значение последнего операнда. Таким образом, j = (++i, i++);, ++i увеличивает i до 6 и i++ возвращает старое значение i (6), которое присваивается j. Тогда i становится 7 из-за постинкремента.

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

printf("%d %d\n", ++i, i++);

не будет проблемой. Но он вызывает неопределенное поведение , потому что запятая здесь представляет собой разделитель .


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

Этот пост: Неопределенное, неопределенное и определяемое реализацией поведение также имеет отношение.

22 голосов
/ 05 декабря 2012

Хотя маловероятно, что какие-либо компиляторы и процессоры действительно будут это делать, в соответствии со стандартом C было бы законно, чтобы компилятор реализовал «i ++» с последовательностью:

In a single operation, read `i` and lock it to prevent access until further notice
Compute (1+read_value)
In a single operation, unlock `i` and store the computed value

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

Если компилятор должен был написать i++, как указано выше (законно в соответствии со стандартом), и перемежать вышеприведенные инструкции во время оценки общего выражения (также законно), и если он не заметил, что одна из других инструкций оказалась доступной к i, и компилятор мог бы (и законно) сгенерировать последовательность инструкций, которые бы зашли в тупик. Безусловно, компилятор почти наверняка обнаружит проблему в случае, когда в обоих местах используется одна и та же переменная i, но если подпрограмма принимает ссылки на два указателя p и q и использует (*p) и (*q) в вышеприведенном выражении (вместо использования i дважды) компилятору не потребуется распознавать или избегать тупиковой ситуации, которая может возникнуть, если один и тот же адрес объекта был передан как для p, так и q.

14 голосов
/ 11 сентября 2014

Стандарт C говорит, что переменная должна присваиваться не более одного раза между двумя точками последовательности. Точка с запятой, например, является точкой последовательности.
Итак, каждое утверждение вида:

i = i++;
i = i++ + ++i;

и т. Д. Нарушают это правило. Стандарт также говорит, что поведение не определено и не определено. Некоторые компиляторы обнаруживают их и дают некоторый результат, но это не соответствует стандарту.

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

while(*src++ = *dst++);

Приведенное выше описание является обычной практикой кодирования при копировании / анализе строк.

13 голосов
/ 26 марта 2017

Хотя синтаксис выражений типа a = a++ или a++ + a++ допустим, поведение этих конструкций равно undefined , поскольку должен в стандарте C не соблюдается. C99 6,5p2 :

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

С сноской 73 , уточняющей, что

  1. Этот параграф отображает неопределенные выражения оператора, такие как

    i = ++i + 1;
    a[i++] = i;
    

    при разрешении

    i = i + 1;
    a[i] = i;
    

Различные точки последовательности перечислены в Приложении C C11 C99 ):

  1. Ниже приведены точки последовательности, описанные в 5.1.2.3:

    .
    • Между оценками обозначения функции и фактическими аргументами в вызове функции и фактическим вызовом. (6.5.2.2).
    • Между оценками первого и второго операндов следующих операторов: логическое И & & (6.5.13); логическое ИЛИ || (6.5.14); запятая, (6.5.17).
    • Между оценками первого операнда условного? : оператор и любой второй и третий операнды оцениваются (6.5.15).
    • Конец полного объявления: объявления (6.7.6);
    • Между оценкой полного выражения и следующим полным выражением, которое будет оценено. Ниже приведены полные выражения: инициализатор, который не является частью составного литерала (6.7.9); выражение в выражении выражения (6.8.3); управляющее выражение оператора выбора (if или switch) (6.8.4); управляющее выражение оператора while или do (6.8.5); каждое из (необязательных) выражений оператора for (6.8.5.3); (необязательное) выражение в операторе возврата (6.8.6.4).
    • Непосредственно перед возвратом библиотечной функции (7.1.4).
    • После действий, связанных с каждым отформатированным спецификатором преобразования функций ввода / вывода (7.21.6, 7.29.2).
    • Непосредственно перед и сразу после каждого вызова функции сравнения, а также между любым вызовом функции сравнения и любым движением объектов, переданных в качестве аргументов для этого вызова (7.22.5).

Текст того же абзаца в C11 :

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

Вы можете обнаружить такие ошибки в программе, например, используя последнюю версию GCC с -Wall и -Werror, и тогда GCC сразу откажется компилировать вашу программу. Ниже приведен вывод gcc (Ubuntu 6.2.0-5ubuntu12) 6.2.0 20161005:

% gcc plusplus.c -Wall -Werror -pedantic
plusplus.c: In function ‘main’:
plusplus.c:6:6: error: operation on ‘i’ may be undefined [-Werror=sequence-point]
    i = i++ + ++i;
    ~~^~~~~~~~~~~
plusplus.c:6:6: error: operation on ‘i’ may be undefined [-Werror=sequence-point]
plusplus.c:10:6: error: operation on ‘i’ may be undefined [-Werror=sequence-point]
    i = (i++);
    ~~^~~~~~~
plusplus.c:14:6: error: operation on ‘u’ may be undefined [-Werror=sequence-point]
    u = u++ + ++u;
    ~~^~~~~~~~~~~
plusplus.c:14:6: error: operation on ‘u’ may be undefined [-Werror=sequence-point]
plusplus.c:18:6: error: operation on ‘u’ may be undefined [-Werror=sequence-point]
    u = (u++);
    ~~^~~~~~~
plusplus.c:22:6: error: operation on ‘v’ may be undefined [-Werror=sequence-point]
    v = v++ + ++v;
    ~~^~~~~~~~~~~
plusplus.c:22:6: error: operation on ‘v’ may be undefined [-Werror=sequence-point]
cc1: all warnings being treated as errors

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

j = (i ++, ++ i);

четко определено и будет увеличивать i на единицу, давая старое значение, отбрасывать это значение; затем в оператор запятой, урегулировать побочные эффекты; и затем увеличиваем i на единицу, и результирующее значение становится значением выражения - то есть это просто надуманный способ записи j = (i += 2), который снова является "умным" способом записи

i += 2;
j = i;

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

int i = 0;
printf("%d %d\n", i++, ++i, i);

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...