Почему 'я = ++ я + 1' неуказанное поведение?

передайте поток битов vera без моно

44
задан Kirill V. Lyadvinsky 7 December 2009 в 17:27
поделиться

14 ответов

Вы ошибаетесь, думая о operator = как о функции с двумя аргументами , где побочные эффекты аргументов должны быть полностью оценены до начала функции. . Если бы это было так, то выражение i = ++ i + 1 имело бы несколько точек последовательности, а ++ i было бы полностью вычислено до начала присваивания. Однако это не так. Что оценивается в внутреннем операторе присваивания , а не в пользовательском операторе. В этом выражении есть только одна точка последовательности.

Результат of ++ i оценивается перед присваиванием (и перед оператором сложения), но побочный эффект не обязательно применяется сразу. Результат ++ i + 1 всегда совпадает с i + 2 , поэтому это значение присваивается i как часть оператора присваивания. . Результатом ++ i всегда является i + 1 , поэтому это то, что присваивается i как часть оператора приращения. Нет точки последовательности, чтобы контролировать, какое значение должно быть присвоено первым.

Поскольку код нарушает правило, что «между предыдущей и следующей точкой последовательности скалярный объект должен иметь свое сохраненное значение, измененное не более одного раза путем оценки выражение "поведение не определено. Однако на практике вполне вероятно, что сначала будет назначено либо i + 1 , либо i + 2 , затем будет присвоено другое значение,

62
ответ дан 26 November 2019 в 21:37
поделиться

я = v [я ++]; // поведение не указано
я = ++ я + 1; // поведение не указано

Все вышеперечисленные выражения вызывают неопределенное поведение.

i = 7, i ++, i ++; // i становится 9

Это нормально.

Прочтите C-FAQs Стива Саммита.

1
ответ дан 26 November 2019 в 21:37
поделиться

Проблема в том, что стандарт позволяет компилятору полностью переупорядочивать оператор во время его выполнения. Однако не разрешается переупорядочивать операторы (при условии, что любое такое переупорядочение приводит к изменению поведения программы). Следовательно, выражение i = ++ i + 1; может быть оценено двумя способами:

++i; // i = 2
i = i + 1;

или

i = i + 1;  // i = 2
++i;

или

i = i + 1;  ++i; //(Running in parallel using, say, an SSE instruction) i = 1

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

1
ответ дан 26 November 2019 в 21:37
поделиться

Основная причина в том, как компилятор обрабатывает чтение и запись значений. Компилятору разрешено сохранять промежуточное значение в памяти и фактически фиксировать значение только в конце выражения. Мы читаем выражение ++ i как «увеличить i на единицу и вернуть его», но компилятор может увидеть его как «загрузить значение i , добавьте один, верните его и зафиксируйте обратно в память, прежде чем кто-то снова его использует. Компилятору рекомендуется по возможности избегать чтения / записи в фактическую ячейку памяти, потому что это замедлит работу программы.

In конкретный случай i = ++ i + 1 , он страдает в основном из-за необходимости согласованных поведенческих правил. Многие компиляторы поступят «правильно» в такой ситуации, но что, если бы один из i был на самом деле указателем, указывающим на i ? Без этого правила компилятору пришлось бы быть очень осторожным, чтобы убедиться, что он выполняет загрузку и сохранение в правильном порядке. Это правило предоставляет больше возможностей для оптимизации.

Похожий случай - так называемое правило строгого сглаживания. Вы не можете присвоить значение (скажем, int ) через значение несвязанного типа (скажем, float ) только за несколькими исключениями. Это избавляет компилятор от беспокойства о том, что некоторые используемые float * изменят значение int и значительно улучшат потенциал оптимизации.

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

Похожий случай - так называемое правило строгого сглаживания. Вы не можете присвоить значение (скажем, int ) через значение несвязанного типа (скажем, float ) только за несколькими исключениями. Это избавляет компилятор от беспокойства о том, что некоторые используемые float * изменят значение int и значительно улучшат потенциал оптимизации.

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

Похожий случай - это так называемое правило строгого сглаживания. Вы не можете присвоить значение (скажем, int ) через значение несвязанного типа (скажем, float ), за некоторыми исключениями. Это избавляет компилятор от беспокойства о том, что некоторые используемые float * изменят значение int и значительно улучшат потенциал оптимизации.

t присваивает значение (скажем, int ) через значение несвязанного типа (скажем, float ) только за несколькими исключениями. Это избавляет компилятор от беспокойства о том, что некоторые используемые float * изменят значение int и значительно улучшат потенциал оптимизации.

t присваивает значение (скажем, int ) через значение несвязанного типа (скажем, float ) только за несколькими исключениями. Это избавляет компилятор от беспокойства о том, что некоторые используемые float * изменят значение int и значительно улучшат потенциал оптимизации.

1
ответ дан 26 November 2019 в 21:37
поделиться

Как насчет того, что мы все согласны никогда, никогда не писать подобный код? Если компилятор не знает, что вы хотите сделать, как вы можете ожидать, что бедняга, который следует за вами, поймет, что вы хотите сделать? Ставим i ++; на своей собственной линии не убьет вас.

2
ответ дан 26 November 2019 в 21:37
поделиться

Из ++ i я должен присвоить "1", но с i = ++ i + 1 ему должно быть присвоено значение «2». Поскольку нет промежуточной точки последовательности, компилятор может предположить, что одна и та же переменная не записывается дважды, поэтому эти две операции могут выполняться в любом порядке. так что да, компилятор будет правильным, если окончательное значение равно 1.

0
ответ дан 26 November 2019 в 21:37
поделиться

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

15
ответ дан 26 November 2019 в 21:37
поделиться

Предположим, вы спрашиваете: «Почему язык разработан таким образом?».

Вы говорите, что i = ++ i + i «очевидно не определено», но i = ++ i + 1 должен оставить i с определенным значением? Честно говоря, это было бы не очень последовательно. Я предпочитаю, чтобы либо все было четко определено, либо все последовательно не уточнялось. В C ++ у меня последняя. Сам по себе это не так уж и плохой выбор - во-первых, он предотвращает написание вредоносного кода, который вносит пять или шесть модификаций в один и тот же «оператор».

4
ответ дан 26 November 2019 в 21:37
поделиться

Важной частью стандарта является:

его сохраненное значение изменяется не более одного раза при вычислении выражения

Вы изменяете значение дважды, один раз с помощью оператора ++, один раз с заданием

8
ответ дан 26 November 2019 в 21:37
поделиться

Следующий код демонстрирует, как можно получить неправильный (неожиданный) результат:

int main()
{
  int i = 0;
  __asm { // here standard conformant implementation of i = ++i + 1
    mov eax, i;
    inc eax;
    mov ecx, 1;
    add ecx, eax;
    mov i, ecx;

    mov i, eax; // delayed write
  };
  cout << i << endl;
}

Это в результате напечатает 1.

4
ответ дан 26 November 2019 в 21:37
поделиться

Аргумент по аналогии: Если вы думаете об операторах как о типах функций, тогда это имеет смысл. Если бы у вас был класс с перегруженным operator = , ваш оператор присваивания был бы эквивалентен примерно так:

operator=(i, ++i+1)

(Первый параметр фактически передается неявно через указатель this , но это просто для иллюстрации.)

Для обычного вызова функции это, очевидно, не определено. Значение первого аргумента зависит от того, когда вычисляется второй аргумент. Однако с примитивными типами это сходит с рук, потому что исходное значение i просто перезаписывается; его стоимость не имеет значения. Но если бы вы творили какую-то другую магию в своем собственном operator = , тогда разница могла бы проявиться.

Проще говоря: все операторы действуют как функции и поэтому должны вести себя в соответствии с одними и теми же понятиями. Если i + ++ i не определено, то i = ++ i также должно быть неопределенным.

3
ответ дан 26 November 2019 в 21:37
поделиться

При наличии двух вариантов: определено или не определено, какой выбор вы бы сделали?

У авторов стандарта было два варианта: определить поведение или указать его как неопределенное.

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

Более того, комитеты по стандартам не имеют возможности заставить авторов компиляторов что-либо делать. Если бы они требовали определенного поведения, вполне вероятно, что это требование было бы проигнорировано.

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

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

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

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

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

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

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

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

9
ответ дан 26 November 2019 в 21:37
поделиться

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

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

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

i = i++ + 1;

Одна запись предназначена для значения исходное значение i «плюс один», а другое - для этого значения снова «плюс один». Эти записи могут происходить в любом порядке или полностью разрушаться, насколько позволяет стандарт. Теоретически это даже дает реализациям свободу выполнять параллельную обратную запись, не беспокоясь о проверке одновременных ошибок доступа.

37
ответ дан 26 November 2019 в 21:37
поделиться

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

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

dim n[10]                 // gives n[0] through n[9]
for each i in 0..9:
    n[i] = i              // initialize them to their indexes
nsize = 10                // starting pool size
do 10 times:
    i = rnd(nsize)        // give a number between 0 and nsize-1
    print n[i]
    nsize = nsize - 1     // these two lines effectively remove the used number
    n[i] = n[nsize]

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

Например, рассмотрите следующую лабораторную проверку:

<--------- n[x] ---------->
for x = 0 1 2 3 4 5 6 7 8 9  nsize  rnd(nsize)  output
---------------------------  -----  ----------  ------
        0 1 2 3 4 5 6 7 8 9     10           4       4
        0 1 2 3 9 5 6 7 8        9           7       7
        0 1 2 3 9 5 6 8          8           2       2
        0 1 8 3 9 5 6            7           6       6
        0 1 8 3 9 5              6           0       0
        5 1 8 3 9                5           2       8
        5 1 9 3                  4           1       1
        5 3 9                    3           0       5
        9 3                      2           1       3
        9                        1           0       9

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

И теперь ваша домашняя работа состоит в том, чтобы превратить это в VB: -)


И, поскольку это домашнее задание сейчас почти наверняка просрочен (около года назад), я

JTC1 / SC22 / WG14 N926 «Анализ точек последовательности»

Кроме того, у Анжелики Лангер есть статья на эту тему (хотя и не столь ясную, как предыдущая):

«Точки последовательности и оценка выражений в C ++»

Также было обсуждение на русском языке (правда, с некоторыми явно ошибочными утверждениями в комментариях и в самом посте):

«Точки следования (точки последовательности)»

7
ответ дан 26 November 2019 в 21:37
поделиться
Другие вопросы по тегам:

Похожие вопросы: