Почему `i = ++ i + 1` не определено поведение? - PullRequest
44 голосов
/ 07 декабря 2009

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

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

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 

- конец примера]

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

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

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

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

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

Ответы [ 15 ]

62 голосов
/ 07 декабря 2009

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

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

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

37 голосов
/ 07 декабря 2009

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

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

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

i = i++ + 1;

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

15 голосов
/ 07 декабря 2009

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

10 голосов
/ 01 октября 2011

Обновление для C ++ 11 (30.09.2011)

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

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

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

Прямо изо рта лошади

http://www.open -std.org / ОТК1 / SC22 / wg21 / документы / cwg_defects.html # 637

9 голосов
/ 07 декабря 2009

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

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

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

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

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

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

8 голосов
/ 07 декабря 2009

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

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

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

7 голосов
/ 07 декабря 2009

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

C ++ Standard Core Language Issue Содержание, Редакция 67, # 351

и

Эндрю Кениг: Ошибка точки последовательности: не определено или не определено?

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

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

++i, ++i; //ok

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

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

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

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

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

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

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

4 голосов
/ 08 декабря 2009

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

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 голосов
/ 07 декабря 2009

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

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

3 голосов
/ 07 декабря 2009

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

operator=(i, ++i+1)

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

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

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

...