Подтвердить что ты не робот

Как определить, что "секвенировано раньше"?

Я прошел этот отличный ответ относительно Undefined Поведение и последовательное взаимодействие [Before/After] в С++ 11. Я понимаю понятия бинарных отношений, но я не понимаю, что такое новые правила, определяющие последовательность.

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

  • i = ++i;
  • a[++i] = i;

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

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

Оператор lhs оператора '=' всегда секвенирован перед rhs и, следовательно, сначала оценивается.

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

4b9b3361

Ответ 1

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

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

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

1. i = ++i;

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

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

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

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

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

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

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

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

Следовательно, мы имеем (b) → (a) → (c) → (d), и это, таким образом, ОК в соответствии с новыми правилами, тогда как в С++ 98 не было ОК.

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

2. a[++i] = i;

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

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

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

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

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

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

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

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

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

Вычисление значения индекса массива ++i является * sequenced-before` вычислением значения выбора элемента, поэтому (b) секвенирован-до (d).

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

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

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

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

Правила

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

  • Вычисления значений аргументов встроенного оператора секвенированы - перед вычислением значения самого оператора.

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

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

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

  • Все вычисления вычислений и побочные эффекты полного выражения секвенированы - перед следующим полным выражением.

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

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

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

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

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

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

Ответ 2

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

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

i = ++i;

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

Повторить:

  • присваивается последовательность после &i и ++i
  • ++i секвенируется после приращения
  • (транзитивность) присваивается секвенция после инкремента

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

Второй случай

a[++i] = i;

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

Повторить:

  • назначение секвенируется после &a[++i] и i
  • &a[++i] секвенируется после ++i
  • ++i секвенируется после приращения

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

Окончательный разглагольствование

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

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

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

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

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