Как определить, что «упорядочено раньше» других? - PullRequest
20 голосов
/ 05 марта 2012

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

Для этих знакомых примеров, как применяются новые правила секвенирования?

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

В частности, Каковы новые правила секвенирования C ++ 11?

Iя ищу некоторые правила, такие как (это полностью составлено)

lhs оператора '=' всегда чередуется перед rhs и, таким образом, оценивается первым.

Если они есть в самом стандарте, может кто-нибудь здесь процитировать то же самое?

Ответы [ 2 ]

13 голосов
/ 06 марта 2012

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

Последствия правил неизменны для простого однопоточного кода.

Давайте начнем с ваших примеров:

1. i = ++i;

Если i является встроенным типом, таким как int, то вызовы функций не задействованы, все является встроенным оператором. Таким образом, происходит 4 вещи:

(a) вычисление значения из ++i, что составляет исходное значение i +1

(b) побочный эффект из ++i, который сохраняет исходное значение-я +1 обратно в i

(c) вычисление значения присвоения, которое является просто сохраненным значением, в этом случае результат вычисления значения из ++i

(d) побочный эффект присвоения, который сохраняет новое значение в i

Все эти вещи секвенированы перед следующим полным выражением. (т. е. все они завершаются последней точкой с запятой утверждения)

Поскольку ++i эквивалентно i+=1, побочный эффект сохранения значения составляет секвенирование до вычисление значения ++i , так что (b) секвенировано до (a).

значение вычисления обоих операндов присваивания равно секвенировано до значение вычислено самого назначения, и это в свою очередь sequenced-before побочный эффект сохранения значения. Поэтому (a) секвенируется перед (c), а (c) секвенируется до (d).

Таким образом, у нас есть (b) -> (a) -> (c) -> (d), и, таким образом, это нормально в соответствии с новыми правилами, тогда как в C ++ 98 это было не так.

Если i было class, то выражение будет i.operator=(i.operator++()) или i.operator=(operator++(i)), и все эффекты вызова operator++ sequenced-before звонок на operator=.

2. a[++i] = i;

Если a является типом массива, а i является int, то снова выражение имеет несколько частей:

(a) Расчет значения из i

(b) Вычисление значения из ++i

(c) Побочный эффект из ++i, который сохраняет новое значение обратно в i

(d) Вычисление значения для a[++i], которое возвращает значение для элемента a, проиндексированного вычислением значения из ++i

(e) вычисление значения присвоения, которое является просто сохраненным значением, в данном случае результат вычисления значения из i

(f) Побочный эффект присваивания *1138*, который сохраняет новое значение в элементе массива a[++i]

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

Опять же, поскольку ++i эквивалентно i+=1, побочный эффект сохранения значения равен sequenced-before вычисление значения of ++i, поэтому (c) является упорядоченным до (b).

значение вычисления индекса массива ++i является * последовательным до 'вычисления значения выбора элемента, поэтому (b) равно последовательным до (д).

значение вычисления обоих операндов присваивания равно последовательность до значение значения самого присвоения, и это в свою очередь sequenced-before побочный эффект сохранения значения. Поэтому (a) и (d) секвенируются до (e), а (e) секвенируются до (f).

Следовательно, у нас есть две последовательности: (a) -> (d) -> (e) -> (f) и (c) -> (b) -> (d) -> (e) -> (f ).

К сожалению, нет порядка между (a) и (c). Таким образом, побочный эффект , который сохраняется в i, является непоследовательным относительно вычисления значения для i, и код демонстрирует неопределенное поведение . Это опять-таки указано в 1.9p15 стандарта C ++ 11.

Как и выше, если i относится к типу класса, то все в порядке, потому что операторы становятся вызовами функций, которые навязывают последовательность.

Правила

Правила относительно просты:

  1. Значения аргументов встроенного оператора: секвенировано до значение самого оператора.

  2. Побочные эффекты встроенного оператора присваивания или оператора преинкремента представляют собой последовательность до вычисление значения результата.

  3. Вычисление значения любого другого встроенного оператора является секвенированным до побочных эффектов этого оператора.

  4. Вычисление значения и побочных эффектов левой части встроенного оператора запятой sequenced- до вычисления значения и побочных эффектов с правой стороны.

  5. Все значения вычисления и побочные эффекты полного выражения sequenced-before следующее полное выражение.

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

  7. Вычисление значения и побочные эффекты всего внутри функция секвенируется перед значением вычисление результата.

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

    Таким образом, в a()+b() либо a() равен секвенировано до b(), либо b() равно секвенировано-до a(), но правила не существует указать какой.

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

  10. Если есть побочный эффект , который изменяет переменную, и вычисление значения , которое читает эту переменную, и ни один из них не является sequenced-before другой код имеет неопределенное поведение.

6 голосов
/ 05 марта 2012

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

Первый случай

i = ++i;

Здесь, чтобы выполнить назначение, вам нужно значение правой части, а чтобы получить это значение, вам нужен побочный эффектуже был применен;поэтому здесь присваивается последовательность после приращения, и все в порядке.Важным моментом здесь является то, что для выполнения назначения вам нужно значение RHS и только адрес LHS.

Подводя итог:

  1. последовательность назначается последовательнопосле того, как &i и ++i
  2. ++i упорядочены после того, как приращение
  3. (транзитивность) упорядочено после приращения

Значение i читается только один раз, после увеличения.Он записывается дважды, один раз с приращением и один раз по присваиванию, но эти две операции последовательно (сначала приращение, затем присвоение).

Второй регистр

a[++i] = i;

Здесь вместовам нужно значение i для RHS и значение ++i для LHS.Однако эти два выражения не упорядочены (оператор присваивания не навязывает упорядочение), и поэтому результат не определен.

Напомним:

  1. присваивание выполняется после &a[++i] иi
  2. &a[++i] секвенируется после ++i
  3. ++i секвенируется после увеличения

Здесь читается значение iдважды, один раз для LHS назначения и один раз для RHS.Часть LHS также делает модификацию (приращение).Этот доступ на запись и доступ на чтение для задания RHS, однако, не упорядочены по отношению друг к другу, и поэтому это выражение имеет вид UB.

Окончательный ответ

Позвольте мне повторить, что я неуверен в том, что я только что сказал ... я твердо убежден в том, что этот новый последовательный подход до / после гораздо сложнее понять.Надеемся, что новые правила сделали только некоторые выражения, которые до этого были хорошо определены UB (а UB - наихудший возможный результат), но также сделали правила намного более сложными (это было просто «не меняйте одно и то же дважды между точками последовательности»).«... вам не нужно было делать умственную топологическую сортировку, чтобы угадать, было ли что-то UB или нет).

В каком-то смысле новые правила не повредили программам на C ++ (UB - враг, и теперь в этой области меньше UB), но он нанес языку , увеличив его сложность (и наверняка что-то, в чем C ++ не нуждался, добавило сложности).

Обратите также внимание на то, что забавным в ++i является то, что возвращаемое значение является l-значением (поэтому ++ ++ i допустимо), так что это в основном адрес, и логически не нужно, чтобы возвращаемое значение было упорядочено послеприращение.Но стандарт говорит так, и это правило, которое вам нужно записать в свои нейроны.Конечно, чтобы иметь «usable» ++i, вы хотите, чтобы пользователи значения получали обновленное значение, но все же, пока оператор ++ видит вещи (он возвращает адрес, который не изменяется приращением), эта последовательностьне был формально необходим.

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

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

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