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

Рассмотрим следующую С++ стандартную ISO/IEC 14882: 2003 (E) цитату (раздел 5, пункт 4):

За исключением тех случаев, когда отмечено, порядок оценка операндов отдельных операторов и подвыражений отдельных выражения и порядок в какие побочные эффекты имеют место, является неопределенные. 53) Между предыдущими и следующая точка последовательности - скаляр объект должен иметь сохраненное значение не более чем один раз оценка выражения. Кроме того, предыдущее значение должно быть доступ только для определения значения для хранения. Требования этого пункт должен быть соблюден для каждого допустимый порядок подвыражения полного выражения; в противном случае поведение undefined. [Пример:

i = v[i++];  // the behavior is unspecified 
i = 7, i++, i++;  //  i becomes 9 

i = ++i + 1;  // the behavior is unspecified 
i = i + 1;  // the value of i is incremented 

-end пример]

Я был удивлен, что i = ++i + 1 дает значение undefined i. Кто-нибудь знает о реализации компилятора, которая не дает 2 для следующего случая?

int i = 0;
i = ++i + 1;
std::cout << i << std::endl;

Дело в том, что operator= имеет два аргумента. Первый всегда является ссылкой i. В этом случае порядок оценки не имеет значения. Я не вижу никаких проблем, кроме С++ Standard taboo.

Пожалуйста,, не учитывайте такие случаи, когда порядок аргументов важен для оценки. Например, ++i + i, очевидно, undefined. Пожалуйста, подумайте только о моем случае i = ++i + 1.

Почему стандарт С++ запрещает такие выражения?

Ответ 1

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

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

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

Ответ 2

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

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

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

i = i++ + 1;

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

Ответ 3

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

Ответ 4

Обновление для С++ 11 (09/30/2011)

Остановить, это четко определено в С++ 11. Это был undefined только в С++ 03, но С++ 11 более гибкий.

int i = 0;
i = ++i + 1;

После этой строки i будет 2. Причина этого изменения была... потому что она уже работает на практике, и было бы больше работы, чтобы она была undefined, чем просто оставить ее определенной в правила С++ 11 (фактически, что это работает в настоящее время, это скорее несчастный случай, чем преднамеренное изменение, поэтому не делать это в своем коде!).

Прямо от устья лошади

http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#637

Ответ 5

Учитывая два варианта: определенный или undefined, какой выбор вы бы сделали?

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

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

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

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

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

Ответ 6

Важная часть стандарта:

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

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

Ответ 7

Обратите внимание, что ваша копия стандарта устарела и содержит известную (и фиксированную) ошибку только в 1-й и 3-й строках кода вашего примера, см.:

Проблема со стандартным ядром С++ Содержание, версия 67, № 351

и

Andrew Koenig: Ошибка точки последовательности: неопределенный или undefined?

Тема не просто получить просто чтение стандарта (что довольно неясно:( в этом случае).

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

++i, ++i; //ok

(++i, ++j) + (++i, ++j); //ub, see the first reference below (12.1 - 12.3)

Пожалуйста, посмотрите (все это ясно и точно):

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

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

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

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

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

Ответ 8

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

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

Ответ 9

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

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.

Ответ 10

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

operator=(i, ++i+1)

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

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

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

Ответ 11

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

Ответ 12

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

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

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

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

Прочитайте ответы на вопросы Стива Стива Summit.

Ответ 13

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

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

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

Ответ 14

Проблема заключается в том, что стандарт позволяет компилятору полностью переупорядочить инструкцию во время ее выполнения. Однако не разрешается переупорядочивать утверждения (пока любое такое переупорядочение приводит к изменению поведения программы). Поэтому выражение 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

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

Ответ 15

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